Handling JSON, Form Data, and File Uploads in Node.js
Master all request body types in Express — JSON, URL-encoded forms, multipart form data, and file uploads with Multer. Includes validation, storage, and security best practices.

Handling JSON, Form Data, and File Uploads in Node.js
Every API receives data from clients — JSON payloads from mobile apps, form submissions from web pages, and files from upload interfaces. Each arrives in a different format, requires different parsing, and carries different security risks.
This module covers all three: how to parse and validate JSON bodies, handle URL-encoded form data, and process file uploads with Multer — including disk storage, memory storage, file type validation, size limits, and uploading to cloud storage.
This is Module 12 of the Node.js Full‑Stack Developer course.
The Three Request Body Types
| Content-Type | Format | Used for |
|---|---|---|
application/json | JSON string | API calls, AJAX, mobile apps |
application/x-www-form-urlencoded | key=value&key2=value2 | HTML form submissions |
multipart/form-data | MIME boundary parts | File uploads + mixed data |
The Content-Type header tells Express which parser to use. Without the right parser, req.body is undefined.
Part 1: JSON Bodies
Parsing JSON
const express = require('express')
const app = express()
// Parse JSON bodies — always add this for API servers
app.use(express.json({ limit: '1mb' }))The limit option prevents clients from sending massive payloads (a common DoS vector). Set it to the largest JSON payload you realistically expect.
Sending JSON from a Client
// Frontend fetch
const response = await fetch('http://localhost:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' }),
})# curl
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'Accessing the Parsed Body
app.post('/api/users', (req, res) => {
console.log(req.body) // { name: 'Alice', email: 'alice@example.com' }
console.log(typeof req.body.name) // 'string'
res.status(201).json({ created: true, user: req.body })
})Handling Malformed JSON
When a client sends malformed JSON, Express throws a SyntaxError. Catch it in your error handler:
// middleware/errorHandler.js
app.use((err, req, res, next) => {
if (err.type === 'entity.parse.failed') {
return res.status(400).json({
success: false,
error: { code: 'INVALID_JSON', message: 'Request body is not valid JSON' },
})
}
// ... other error handling
})Part 2: URL-Encoded Form Data
HTML forms without enctype="multipart/form-data" submit as URL-encoded:
<form method="POST" action="/contact">
<input name="name" value="Alice" />
<input name="email" value="alice@example.com" />
<button type="submit">Send</button>
</form>This sends: name=Alice&email=alice%40example.com
Parsing URL-Encoded Data
app.use(express.urlencoded({ extended: true, limit: '1mb' }))extended: true— uses theqslibrary, supports nested objects (user[name]=Alice)extended: false— uses the built-inquerystring, flat key-value only
app.post('/contact', (req, res) => {
const { name, email, message } = req.body
console.log(name, email, message)
res.redirect('/thank-you')
})Nested Form Data (extended: true)
<input name="address[city]" value="London" />
<input name="address[postcode]" value="EC1A 1BB" />console.log(req.body)
// { address: { city: 'London', postcode: 'EC1A 1BB' } }Part 3: File Uploads with Multer
File uploads use multipart/form-data — a format Express cannot parse natively. Multer handles it.
npm install multerHow Multer Works
Client sends multipart/form-data
│
▼
Multer parses the stream
│
┌──────┴──────┐
│ │
Files Text fields
│ │
req.file req.body
req.filesreq.file— single uploaded filereq.files— multiple uploaded filesreq.body— text fields from the same form
Storage Options
Multer provides two built-in storage engines:
Disk Storage
Saves files directly to the local filesystem:
const multer = require('multer')
const path = require('path')
const { randomUUID } = require('crypto')
const diskStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/') // must exist — create with fs.mkdir first
},
filename: (req, file, cb) => {
// Use UUID + original extension to prevent filename collisions
const ext = path.extname(file.originalname).toLowerCase()
cb(null, `${randomUUID()}${ext}`)
},
})Memory Storage
Keeps files in memory as a Buffer — ideal when you immediately upload to cloud storage:
const memoryStorage = multer.memoryStorage()
// file will be available as req.file.bufferFile Type Validation
Always whitelist accepted MIME types — never rely on file extensions alone:
const ALLOWED_MIME_TYPES = new Set([
'image/jpeg',
'image/png',
'image/webp',
'image/gif',
])
const fileFilter = (req, file, cb) => {
if (ALLOWED_MIME_TYPES.has(file.mimetype)) {
cb(null, true) // accept
} else {
cb(new Error(`File type '${file.mimetype}' is not allowed`), false)
}
}Putting It Together
const multer = require('multer')
const path = require('path')
const { randomUUID } = require('crypto')
const fs = require('fs/promises')
// Ensure uploads directory exists
await fs.mkdir('uploads', { recursive: true })
const ALLOWED_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'])
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase()
cb(null, `${randomUUID()}${ext}`)
},
}),
fileFilter: (req, file, cb) => {
if (ALLOWED_MIME_TYPES.has(file.mimetype)) {
cb(null, true)
} else {
cb(Object.assign(new Error('Invalid file type'), { status: 422 }))
}
},
limits: {
fileSize: MAX_FILE_SIZE, // max file size
files: 1, // max number of files
fields: 10, // max non-file fields
},
})Upload Patterns
Single File Upload
// Route: POST /api/avatar
// Form field name must match: upload.single('avatar')
app.post('/api/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' })
}
console.log(req.file)
// {
// fieldname: 'avatar',
// originalname: 'photo.jpg',
// encoding: '7bit',
// mimetype: 'image/jpeg',
// destination: 'uploads/',
// filename: 'a3f2c8d1-...-avatar.jpg',
// path: 'uploads/a3f2c8d1-...-avatar.jpg',
// size: 204800 ← bytes
// }
res.json({
success: true,
file: {
url: `/uploads/${req.file.filename}`,
size: req.file.size,
type: req.file.mimetype,
},
})
})HTML form:
<form action="/api/avatar" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*" />
<button type="submit">Upload</button>
</form>Multiple Files Upload
// Up to 5 files in a field named 'photos'
app.post('/api/photos', upload.array('photos', 5), (req, res) => {
const files = req.files.map(file => ({
url: `/uploads/${file.filename}`,
size: file.size,
type: file.mimetype,
}))
res.status(201).json({ success: true, files })
})Mixed Fields and Files
// Multiple different file fields
const cpUpload = upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 6 },
])
app.post('/api/profile', cpUpload, (req, res) => {
const avatar = req.files['avatar']?.[0]
const gallery = req.files['gallery'] || []
const { bio, website } = req.body // text fields still in req.body
res.json({ avatar, galleryCount: gallery.length, bio, website })
})Uploading to Cloud Storage (AWS S3)
For production, stream files directly to S3 instead of saving to disk:
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage// lib/s3.js
const { S3Client, DeleteObjectCommand } = require('@aws-sdk/client-s3')
const { Upload } = require('@aws-sdk/lib-storage')
const { randomUUID } = require('crypto')
const path = require('path')
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
})
const BUCKET = process.env.S3_BUCKET
async function uploadToS3(file, folder = 'uploads') {
const ext = path.extname(file.originalname).toLowerCase()
const key = `${folder}/${randomUUID()}${ext}`
const upload = new Upload({
client: s3,
params: {
Bucket: BUCKET,
Key: key,
Body: file.buffer, // from memoryStorage
ContentType: file.mimetype,
ACL: 'public-read',
},
})
await upload.done()
return {
key,
url: `https://${BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`,
}
}
async function deleteFromS3(key) {
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key }))
}
module.exports = { uploadToS3, deleteFromS3 }// Use memoryStorage — keep file in memory, upload to S3
const uploadMemory = multer({
storage: multer.memoryStorage(),
fileFilter,
limits: { fileSize: MAX_FILE_SIZE },
})
const { uploadToS3 } = require('../lib/s3')
app.post('/api/avatar', uploadMemory.single('avatar'), async (req, res, next) => {
try {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' })
const { url, key } = await uploadToS3(req.file, 'avatars')
// Save url and key to your database
await User.findByIdAndUpdate(req.user.id, { avatarUrl: url, avatarKey: key })
res.json({ success: true, url })
} catch (err) {
next(err)
}
})Handling Multer Errors
Multer errors (file too large, wrong type, too many files) need special handling because they do not go through next(err) automatically:
const multer = require('multer')
// Wrap multer in a promise for async/await error handling
function runMiddleware(req, res, middleware) {
return new Promise((resolve, reject) => {
middleware(req, res, (err) => {
if (err) reject(err)
else resolve()
})
})
}
app.post('/api/avatar', async (req, res, next) => {
try {
await runMiddleware(req, res, upload.single('avatar'))
} catch (err) {
if (err instanceof multer.MulterError) {
// Multer-specific errors
const messages = {
LIMIT_FILE_SIZE: 'File is too large (max 5MB)',
LIMIT_FILE_COUNT: 'Too many files uploaded',
LIMIT_UNEXPECTED_FILE: 'Unexpected file field name',
}
return res.status(422).json({
success: false,
error: { code: 'UPLOAD_ERROR', message: messages[err.code] || err.message },
})
}
return next(err) // custom errors (invalid type etc.)
}
if (!req.file) return res.status(400).json({ error: 'No file provided' })
res.json({ success: true, filename: req.file.filename })
})Complete Upload Route — Production Pattern
// routes/v1/uploads.js
const express = require('express')
const multer = require('multer')
const { uploadToS3 } = require('../../lib/s3')
const authenticate = require('../../middleware/auth')
const User = require('../../models/User')
const router = express.Router()
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'])
const upload = multer({
storage: multer.memoryStorage(),
fileFilter: (req, file, cb) => {
ALLOWED_TYPES.has(file.mimetype)
? cb(null, true)
: cb(Object.assign(new Error('Only JPEG, PNG, and WebP images are allowed'), { status: 422 }))
},
limits: { fileSize: 5 * 1024 * 1024, files: 1 },
})
function handleUpload(field) {
return (req, res, next) => {
upload.single(field)(req, res, err => {
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
return res.status(422).json({ error: 'File must be under 5MB' })
}
if (err) return next(err)
next()
})
}
}
// POST /api/v1/uploads/avatar
router.post('/avatar', authenticate, handleUpload('avatar'), async (req, res, next) => {
try {
if (!req.file) return res.status(400).json({ error: 'No file provided' })
const { url, key } = await uploadToS3(req.file, 'avatars')
await User.findByIdAndUpdate(req.user.id, { avatarUrl: url })
res.status(201).json({ success: true, data: { url } })
} catch (err) {
next(err)
}
})
module.exports = routerNode.js Full‑Stack Course — Module 12 of 32
You can now handle every type of client data — JSON, forms, and files. Continue to Module 13 to learn the Express Router for structuring large applications.
Summary
Handling client data correctly is fundamental to every API. The key takeaways:
express.json({ limit: '1mb' })parses JSON bodies — always set a size limitexpress.urlencoded({ extended: true })parses HTML form submissions- Multer handles
multipart/form-datafile uploads — Express cannot do this natively - Always whitelist MIME types in
fileFilter— never trust file extensions - Set
limits.fileSizeto prevent large upload attacks - Use
memoryStorage+ cloud upload (S3/R2) in production — never rely on local disk - Wrap Multer in a promise for clean async/await error handling
- Handle
MulterErrorseparately from other errors — it has its own error codes
Continue to Module 13: Express Router — Structuring Large Applications →
Content powered by SearchFit.ai — for automated content at scale, visit https://searchfit.ai
