Project: File Upload Server
Build an HTTP file upload server using Node.js built-in http module, multipart/form-data parsing, and fs streams
Overview
Key Points
- β’http.createServer receives an IncomingMessage (request) and ServerResponse (response) for every connection
- β’The request body arrives as a Readable stream β collect it with a "data" event loop or use stream/promises
- β’multipart/form-data bodies contain multiple parts separated by a boundary string found in the Content-Type header
- β’Each multipart part has a Content-Disposition header containing the field name and filename
- β’File bytes begin after the blank line following the part headers and end before the next boundary
- β’Always validate file size, extension, and MIME type before writing to disk to prevent path traversal and oversized uploads
Examples
The finished server β test it with curl or a browser form
# Start the server:
node server.js
# Listening on http://localhost:3000
# Upload a file with curl:
curl -X POST http://localhost:3000/upload \
-F "file=@./photo.jpg"
# {"success":true,"filename":"photo.jpg","size":204800,"path":"/uploads/photo.jpg"}
# Or open http://localhost:3000 in a browser β it serves the upload form
# List uploaded files:
curl http://localhost:3000/files
# {"files":["photo.jpg","report.pdf"]}Creating the HTTP server and routing requests
const http = require('http');
const fs = require('fs');
const path = require('path');
const UPLOAD_DIR = path.join(__dirname, 'uploads');
fs.mkdirSync(UPLOAD_DIR, { recursive: true }); // ensure uploads/ exists
const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (method === 'GET' && url === '/') return serveForm(res);
if (method === 'GET' && url === '/files') return listFiles(res);
if (method === 'POST' && url === '/upload') return handleUpload(req, res);
res.writeHead(404); res.end('Not found');
});
server.listen(3000, () => console.log('Listening on http://localhost:3000'));Collecting the multipart body and extracting the boundary
async function handleUpload(req, res) {
const contentType = req.headers['content-type'] || '';
// Extract boundary: multipart/form-data; boundary=----WebKit...
const boundaryMatch = contentType.match(/boundary=(.+)/);
if (!boundaryMatch) return sendJson(res, 400, { error: 'No boundary found' });
const boundary = boundaryMatch[1];
// Collect the full body (Buffer, not string β files are binary)
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = Buffer.concat(chunks);
// Parse out the file part
const file = parseMultipart(body, boundary);
if (!file) return sendJson(res, 400, { error: 'No file in request' });
// Validate extension
const ext = path.extname(file.filename).toLowerCase();
if (!['.jpg', '.jpeg', '.png', '.pdf', '.txt'].includes(ext))
return sendJson(res, 400, { error: 'File type not allowed' });
const savePath = path.join(UPLOAD_DIR, file.filename);
await fs.promises.writeFile(savePath, file.data);
sendJson(res, 200, { success: true, filename: file.filename, size: file.data.length });
}Parsing the multipart body β splitting on boundary and extracting file bytes
function parseMultipart(body, boundary) {
const delimiter = Buffer.from('--' + boundary);
const parts = splitBuffer(body, delimiter);
for (const part of parts) {
// Each part starts with headers, then \r\n\r\n, then the data
const headerEnd = part.indexOf(Buffer.from('\r\n\r\n'));
if (headerEnd === -1) continue;
const headerStr = part.slice(0, headerEnd).toString();
const data = part.slice(headerEnd + 4); // skip \r\n\r\n
// Content-Disposition: form-data; name="file"; filename="photo.jpg"
const nameMatch = headerStr.match(/filename="([^"]+)"/);
if (!nameMatch) continue; // not a file part
// Strip trailing \r\n before the next boundary
const fileData = data.slice(0, data.length - 2);
return { filename: path.basename(nameMatch[1]), data: fileData };
}
return null;
}
function splitBuffer(buf, delimiter) {
const parts = [];
let offset = 0;
let idx;
while ((idx = buf.indexOf(delimiter, offset)) !== -1) {
parts.push(buf.slice(offset, idx));
offset = idx + delimiter.length;
}
parts.push(buf.slice(offset));
return parts.filter(p => p.length > 2);
}Common Mistakes
- Reading the request body with req.on("data") using toString() β uploaded files are binary data and toString() corrupts non-UTF-8 bytes; always collect Buffer chunks and concat them
- Using string split() on the multipart body β the boundary must be searched as a Buffer to avoid corrupting binary file content
- Skipping file extension validation β a user could upload a .exe, .php, or a 1 GB file; always whitelist extensions and check Content-Length if available
- Not handling CORS headers β browsers block cross-origin form submissions by default; add Access-Control-Allow-Origin if the form is served from a different origin
Note
For production file uploads use the Multer npm package with Express β it handles multipart parsing, streaming to disk, file size limits, and extension filtering robustly. This project shows what Multer does internally; in real apps you should not reinvent it.
Exercises
Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.
Completed: 0 / 6 exercises
Knowledge Check
Question 1 of 4