Middleware and input

Parse only expected bodies

Enable JSON and URL-encoded parsers with explicit size limits where routes need them.

Parsing a body costs memory and CPU before your code has checked anything. That makes the parser part of the security boundary. Decide which routes accept a body, which format, and how big, and say so in code.

The notes app needs two parsers. The JSON API accepts small JSON documents. The HTML forms send URL-encoded fields. No route accepts both, and none accepts an unlimited body.

Mount the parser where the body is expected

Instead of one global app.use(express.json()), each router gets the parser it needs:

apiRouter.use(express.json({ limit: '10kb' }))

pagesRouter.use(express.urlencoded({ extended: false, limit: '10kb' }))

A note is a title and some text. 10 KB is plenty, and the default of 100 KB is ten times more than we’ll ever need. Pick a number from the data, not from the docs.

extended: false uses Node’s own query-string parser, which produces flat objects. In Express 5 that is the default anyway, but I write it down so nobody wonders.

Four requests, four answers

Start the server and try the cases that matter. A valid body first:

curl -i -X POST http://localhost:3000/api/notes \
  -H 'Content-Type: application/json' -d '{"title":"Buy milk"}'

You get 201. Now malformed JSON:

curl -i -X POST http://localhost:3000/api/notes \
  -H 'Content-Type: application/json' -d '{"title":'

The parser throws an error with status: 400 and type: 'entity.parse.failed'. It never reaches your handler. Then an oversized body:

head -c 20000 /dev/zero | tr '\0' 'a' | \
  curl -i -X POST http://localhost:3000/api/notes \
  -H 'Content-Type: application/json' --data-binary @-

That’s status: 413 and type: 'entity.too.large'. The parser stops reading at the limit, so a 2 GB upload costs you 10 KB.

The fourth case is the quiet one. Send the right data with the wrong content type:

curl -i -X POST http://localhost:3000/api/notes \
  -H 'Content-Type: text/plain' -d '{"title":"Buy milk"}'

express.json() only parses application/json. It sees text/plain and calls next() without touching the request. In Express 5, req.body stays undefined. Your handler must decide what that means.

Reject the wrong type before domain work

I add one small middleware after the parser on the API router:

apiRouter.use((req, res, next) => {
  if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body === undefined) {
    return res.status(415).json({ error: 'Expected application/json' })
  }
  next()
})

415 Unsupported Media Type is the precise answer. Without it, the handler reads req.body.title, throws a TypeError, and the client gets a 500 for what is a client mistake.

Let the error middleware translate

The 400 and 413 errors from the parser flow into the error middleware like any other error. Map them by err.type there, so the API answers with a consistent JSON shape instead of the default HTML error page. We build that handler in the errors module.

Try this: lower the limit to '100b', send a normal note, and watch a valid request fail with 413. It’s a good reminder that the limit is a product decision, not a security knob you crank down blindly.

Lesson completed