0% completed
Working with Built-in Middleware
On This Page
Reading JSON with express.json()
Reading HTML Forms with express.urlencoded()
Serving Files with express.static()
Reading Plain Text and Raw Bytes
Which Parser Reads Which Request
The handler below prints the body of every POST /debug request. A client sends it the JSON body {"title":"Buy milk"} with the correct Content-Type header, and the handler prints undefined:
const express = require('express'); const app = express(); app.post('/debug', (req, res) => { console.log(req.body); res.sendStatus(204); });
undefined
The body did arrive, but Express does not read it. Reading a body means collecting the bytes from the network and converting them into a value, and Express does that work only when a middleware function asks for it. Until such a function runs, req.body is undefined. Express 5 also keeps it undefined when a body parser runs but skips the request because the Content-Type does not match. In Express 4, a parser that skipped a request set req.body to an empty object, so older code sometimes checks for {}.
The method res.sendStatus(204) sends the status code with no body. The status 204 No Content means that the request succeeded and that there is nothing to return.
Express ships with five middleware functions for this kind of work. Four of them read request bodies, and the fifth serves files from a folder. They are built in, so you do not install anything: older tutorials install the body-parser package separately, but Express 5 already includes it and exposes it as express.json() and the related functions.
Reading JSON with express.json()
The task API reads new tasks as JSON. Register the JSON parser before the routes that read req.body:
app.use(express.json()); app.post('/tasks', (req, res) => { const task = { id: tasks.length + 1, title: req.body.title, done: false }; tasks.push(task); res.status(201).json(task); });
For each request, express.json() first looks at the Content-Type header. When the header says application/json, the parser reads the whole body, parses it, stores the result in req.body, and calls next(). When the header names any other type, the parser calls next() at once and leaves req.body untouched. That check is cheap, so registering the parser for the whole app costs almost nothing on requests that have no JSON body.
The parser can also reject a request. When it does, it does not call your route. It passes an error to next(err), and the error carries a status property with the HTTP status code to send:
- A body that is not valid JSON, such as
{"title":, produces status 400 with the messageUnexpected end of JSON input. - A body larger than the limit produces status 413 with the message
request entity too large. The default limit is 100 KB, and a JSON body of about 200 KB was rejected in a test, while one of about 90 KB was accepted. You can change the limit with an option, as inexpress.json({ limit: '10kb' }). - A body whose top level is not an object or an array, such as the JSON string
"just a string", produces status 400. The parser accepts only objects and arrays at the top level unless you set the optionstrict: false.
Without an error handler of your own, Express answers these errors with its HTML error page. An error handler, introduced in the previous lesson, can send JSON instead, and it can use the status property that the parser set:
app.use((err, req, res, next) => { const status = err.status || 500; const message = status < 500 ? err.message : 'Something went wrong'; res.status(status).json({ error: message }); });
With this handler registered after the routes, the invalid body {"title": now receives a JSON response:
HTTP/1.1 400 Bad Request Content-Type: application/json; charset=utf-8 {"error":"Unexpected end of JSON input"}
The handler repeats the parser's message only for status codes below 500. Those errors describe a mistake in the request, so the client can use the message to fix it. A status of 500 or more means a fault in the server, and its message may reveal internal details, so the client receives a general message instead.
Reading HTML Forms with express.urlencoded()
A browser sends a plain HTML form in a different format. The form below adds a task:
<form action="/tasks" method="post"> <input name="title" placeholder="New task"> <button type="submit">Add task</button> </form>
When you type Buy milk and submit the form, the browser sends a POST /tasks request with the header Content-Type: application/x-www-form-urlencoded and the body title=Buy+milk. In this format, called URL-encoded, each field becomes a name=value pair. The pairs are joined with &, and a space becomes +. The JSON parser ignores this request because of its Content-Type, so the app needs a second parser. Register it next to the JSON parser, above the routes:
app.use(express.urlencoded());
Now the same POST /tasks route receives req.body as { title: 'Buy milk' }, and the handler from the JSON section works for both kinds of request without any change. Every value from a form is a string, as with query parameters.
The parser has one option that changes its result. With the default, extended: false, it does not interpret brackets in field names, so a field named tags[] stays the key 'tags[]'. With extended: true, it turns names with brackets into arrays and nested objects, so tags[]=home&tags[]=urgent becomes { tags: ['home', 'urgent'] }. In Express 4 the default was true, and Express printed a warning when the option was missing. Express 5 changed the default to false, so write express.urlencoded({ extended: true }) only when your forms really send nested field names.
Serving Files with express.static()
The form needs to be served from somewhere. Save it as public/index.html, together with any style sheets or images, and let Express serve that whole folder:
const path = require('node:path'); app.use(express.static(path.join(__dirname, 'public')));
For each GET or HEAD request, express.static() looks for a file in public whose path matches the request path. A request for /styles.css returns public/styles.css, and a request for / returns public/index.html, because the middleware serves index.html for a folder. Express sets the Content-Type from the file extension and adds headers for caching:
HTTP/1.1 200 OK Accept-Ranges: bytes Cache-Control: public, max-age=0 ETag: W/"22-1a1ade1570b" Content-Type: text/css; charset=utf-8 Content-Length: 34
When no file matches, the middleware calls next(), so the request continues to the routes below it. That is why the static middleware and the /tasks routes can share one app.
Three details are worth knowing before you rely on it:
- Build the folder path with
path.join(__dirname, 'public'). A plain'public'is resolved from the folder where you startednode, so the app would serve the wrong folder, or nothing, if you started it from another directory. - You can put the files under a prefix with
app.use('/static', express.static(...)). The same file is then served at/static/styles.css. - Files whose names start with a dot, such as
.env, are ignored by default. A request for/.envreceived a 404 response in a test, even thoughpublic/.envexisted. Express 5 also ignores files inside a folder whose name starts with a dot, such aspublic/.well-known/, which Express 4 still served. If you need such a folder, mount a separate static middleware for it, as inapp.use('/.well-known', express.static(path.join(__dirname, 'public', '.well-known'))). Still, keep secrets out of the public folder.
Reading Plain Text and Raw Bytes
The last two parsers cover bodies that are neither JSON nor forms.
express.text() reads a body with Content-Type: text/plain and stores it in req.body as a string. Instead of registering it for the whole app, you can pass it to one route, which then is the only route that accepts text:
app.post('/tasks/:id/notes', express.text(), (req, res) => { res.status(201).json({ taskId: Number(req.params.id), note: req.body }); });
A request with the text body Call before noon to /tasks/2/notes receives {"taskId":2,"note":"Call before noon"}.
express.raw() reads a body with Content-Type: application/octet-stream and stores it in req.body as a Buffer, which is the Node.js type for a sequence of bytes. It does not convert the bytes in any way. A common use is a webhook, which is a request that another service, such as a payment provider, sends to your app when an event happens. Many providers sign each webhook, and checking the signature requires the exact bytes that the provider sent, before any JSON parsing:
app.post('/webhooks/payments', express.raw({ type: 'application/json' }), (req, res) => { console.log(Buffer.isBuffer(req.body), req.body.length); res.sendStatus(204); });
The type option tells the parser which Content-Type to read, so this route receives the JSON body {"event":"paid"} as a 16-byte Buffer and prints true 16. Register this route before app.use(express.json()). A body can be read only once, so when the JSON parser has already run, the raw parser leaves req.body as the parsed object, and the route receives no bytes to check.
Which Parser Reads Which Request
When all four parsers are registered for the whole app, each request passes all of them, and each parser acts only when the Content-Type header matches its own type. The same POST route printed these values of req.body for five requests:
application/json -> object { title: 'Buy milk' } application/x-www-form-urlencoded -> object { title: 'Buy milk' } text/plain -> string 'Call before noon' application/octet-stream -> Buffer <Buffer 01 02 03 04> multipart/form-data -> undefined undefined
Trace each row of the diagram from left to right, and notice that the last request passes all four parsers without a match. A multipart/form-data body is what a browser sends when a form uploads a file. None of the built-in parsers reads it, and chapter 7 handles it with the Multer package.
Reading Progress
0%
On This Page
Reading JSON with express.json()
Reading HTML Forms with express.urlencoded()
Serving Files with express.static()
Reading Plain Text and Raw Bytes
Which Parser Reads Which Request