Handling JSON, Form Data, and File Uploads in Node.js

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
