Node.jsExpressBackendFull-Stack

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

TT
TopicTrick Team
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-TypeFormatUsed for
application/jsonJSON stringAPI calls, AJAX, mobile apps
application/x-www-form-urlencodedkey=value&key2=value2HTML form submissions
multipart/form-dataMIME boundary partsFile 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

javascript
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

javascript
// 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' }),
})
bash
# 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

javascript
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:

javascript
// 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:

html
<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

javascript
app.use(express.urlencoded({ extended: true, limit: '1mb' }))
  • extended: true — uses the qs library, supports nested objects (user[name]=Alice)
  • extended: false — uses the built-in querystring, flat key-value only
javascript
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)

html
<input name="address[city]" value="London" />
<input name="address[postcode]" value="EC1A 1BB" />
javascript
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.

bash
npm install multer

How Multer Works

text
Client sends multipart/form-data
           │
           ▼
      Multer parses the stream
           │
    ┌──────┴──────┐
    │             │
  Files        Text fields
    │             │
req.file       req.body
req.files
  • req.file — single uploaded file
  • req.files — multiple uploaded files
  • req.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:

javascript
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:

javascript
const memoryStorage = multer.memoryStorage()
// file will be available as req.file.buffer

File Type Validation

Always whitelist accepted MIME types — never rely on file extensions alone:

javascript
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

javascript
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

javascript
// 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:

html
<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

javascript
// 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

javascript
// 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:

bash
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage
javascript
// 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 }
javascript
// 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:

javascript
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

javascript
// 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 = router

Node.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 limit
    • express.urlencoded({ extended: true }) parses HTML form submissions
    • Multer handles multipart/form-data file uploads — Express cannot do this natively
    • Always whitelist MIME types in fileFilter — never trust file extensions
    • Set limits.fileSize to 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 MulterError separately 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