This episode covers file uploads and media: processing multipart forms with multer, limiting file size and type, streaming files from disk with sendFile, and supporting range requests for video and audio.

Almost every modern application needs to accept files: profile photos, document attachments, or videos uploaded by users. Managing uploads correctly isn't just about storing bytes — you must validate content, limit size, and stream large files without weighing on memory.
Episode 17 covers file and media management in Node.js: processing multipart forms with multer, limiting file size and type, streaming files from disk to the response, and supporting range requests so videos can be played from the middle. These are the skills a media API needs.
Files can't be sent as plain JSON. Browsers send files in multipart/form-data, a format that separates regular fields and files in one request. Multer is an Express middleware that processes this format:
npm install multernpm install multer adds the upload middleware. Multer parses the multipart body and provides uploaded files through req.file or req.files.
import multer from "multer";
const upload = multer({ dest: "uploads/" });
app.post("/api/upload", upload.single("gambar"), (req, res) => {
console.log(req.file.originalname);
res.status(201).json({
nama: req.file.originalname,
ukuran: req.file.size,
path: req.file.path,
});
});upload.single("gambar") captures a single file from the field named gambar. After the middleware runs, req.file holds the file metadata: originalname, size, and path, where the file is stored on disk. For multiple files, use upload.array("gambar", 5).
Files without limits are a security and resource-abuse risk. Multer allows validating names and types, along with a size limit at the Express level:
const upload = multer({
dest: "uploads/",
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (file.mimetype === "image/png" || file.mimetype === "image/jpeg") {
cb(null, true);
} else {
cb(new Error("Tipe file tidak diizinkan"));
}
},
});limits: { fileSize: 5 * 1024 * 1024 } rejects files over 5 megabytes, and fileFilter only accepts PNG and JPEG. Remember: mimetype can be forged by the client — in production, additional validation by reading the file's magic bytes is a safer practice (we'll discuss that in episode 20).
When a limit is exceeded or a type is rejected, multer's error is sent to the error handler:
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({ error: "Upload gagal: " + err.message });
}
next(err);
});err instanceof multer.MulterError detects multer-specific errors like LIMIT_FILE_SIZE. Turning them into a clear 400 response is much better than letting a raw error leak to the client.
When sending a file back to the client, don't read the whole thing into memory. Use a stream or res.sendFile, which flows the file directly from disk:
import { createReadStream } from "node:fs";
app.get("/api/download/:nama", (req, res) => {
res.download("uploads/" + req.params.nama);
});res.download(path) sends the file with a Content-Disposition header that triggers a download in the browser, while res.sendFile displays it inline. Both use streaming behind the scenes — large files are never fully loaded into memory. Sanitizing req.params.nama is important so users can't break through to other file paths, a topic we'll cover in episode 20.
app.get("/api/video/:nama", (req, res) => {
const stream = createReadStream("uploads/" + req.params.nama);
stream.pipe(res);
});createReadStream(...) reads the file in small chunks and stream.pipe(res) flows them to the response. This approach keeps server memory low even while serving large videos simultaneously.
Browsers play videos by sending a range request — a request for only part of a file (for example, bytes 1000 to 5000). Without range support, videos can't be skipped. A correct server must understand the Range header and reply with status 206:
import { stat } from "node:fs/promises";
app.get("/api/video/:nama", async (req, res) => {
const berkas = "uploads/" + req.params.nama;
const info = await stat(berkas);
res.status(200);
res.setHeader("Content-Length", info.size);
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Type", "video/mp4");
createReadStream(berkas).pipe(res);
});The Accept-Ranges: bytes header tells the browser that the server supports partial files. Full Range support — parsing the header, replying with 206 Partial Content, and mapping stream positions — can get complex; in practice, consider middleware like express-serve-static-core or a CDN that already handles this maturely.
Here's what to take away:
limits.fileSize and fileFilter limit file size and type.res.download and res.sendFile stream files from disk.createReadStream and pipe.Accept-Ranges header and 206 support video playback.In the next episode, episode 18, we'll discuss API testing and integration with modern tools — the built-in node:test runner, testing HTTP endpoints with supertest, mocking for isolation, and measuring coverage. You'll lock down your API so changes don't break features.