Response
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Express 5.3 (npm express 5.3.0, published 2026-10-09) on Node.js 18 or later, the minimum
for Express 5, and it states where Express 4 (latest 4.22.3) behaves differently. Everything was checked against the
official API reference on 2026-10-29. The response object is the second argument of every route handler and middleware
function. Express builds it by adding a set of helper methods to Node.js’s http.ServerResponse, so everything Node
provides (res.write(), res.writeHead(), res.setHeader(), res.getHeader(), res.removeHeader(),
res.flushHeaders(), res.statusCode, res.writableEnded and the finish and close events) keeps working next to
the Express helpers. The examples all belong to the Bookshelf API that grows across the section, use ESM and the node:
prefix for built-in modules, and contain placeholders only, never real secrets.
The Express team keeps the 5.1 to 5.3 releases deliberately small. Looking at the release notes, the res method
signatures documented here did not change between 5.0 and 5.3; what moved are the dependencies that implement them
(send for files, cookie for cookies, qs, proxy-addr and body-parser elsewhere). The Express 5 changes that do
matter, listed in Removed and changed in Express 5, all arrived with 5.0.
The helpers at a glance
Nearly every helper returns res, so calls chain (res.status(201).location(url).json(book)). Only the helpers that
finish the response (send, json, jsonp, end, redirect, render without a callback, sendFile, download,
sendStatus, format) are terminal: nothing useful can be done with the response after calling them, and calling a
second one is the classic "headers already sent" bug (res.headersSent and the double-response bug).
| Need | Helper | Section |
|---|---|---|
Set the status line |
|
|
Read or write headers |
|
|
Send a body |
|
Sending a body: |
Send a file |
|
|
Redirect |
|
|
Render HTML from a template |
|
|
Pick a representation from |
|
|
Cookies |
|
|
Share data with later layers and views |
|
Properties
| Property | Type | Meaning |
|---|---|---|
|
|
The Express application that is using the middleware. It is the same object as |
|
|
The request that belongs to this response; |
|
|
|
|
|
Variables scoped to this request and visible to the views rendered during it. They are not shared between requests; use
|
res.locals is the standard place for middleware to hand data to a later handler or template. Its role in a pipeline is
explained in Middleware Fundamentals, and
Template Engines and Views shows how res.render() merges it with the
locals of the call.
Status: res.status() and res.sendStatus()
res.status(code) sets the numeric status code and returns res so that it can be chained. It is the chainable form of
Node’s res.statusCode. Express 5 validates the argument: the code must be an integer from 100 to 999, and anything else
(a string such as '404', a float, undefined, NaN, 1000) throws an error instead of silently producing a broken
response. In Express 4 an invalid code failed later and less clearly, inside Node.
res.sendStatus(code) sets the status and sends the standard reason phrase as a plain-text body (res.sendStatus(404)
sends Not Found). For a code Node does not know, the body is the number itself. It is a shortcut for tiny responses such
as health checks (res.sendStatus(204)) and is not suited to an API that returns structured errors; for those, use
res.status(code).json(problem).
Pick the status with care: 200 for a successful read, 201 plus a Location header for a created resource, 204 for a
success with no body (Express strips the body and the Content-Type, Content-Length and Transfer-Encoding headers
for 204 and 304), 400 and 422 for client mistakes, 401 and 403 for authentication and authorization failures,
404 for a missing resource, 409 for conflicts, and 5xx for faults on the server. The full status-code conventions of
a REST API are covered in Building REST APIs.
Headers
res.get(), res.set() and res.header()
res.get(field) returns the value of a response header that was already set; the name is case-insensitive.
res.set(field, value) sets a header, replacing any previous value, and res.set(object) sets several headers at once;
res.header() is an alias. A value can be a string or an array of strings (an array produces several lines of the same
header). When the field is Content-Type, Express adds charset=utf-8 to a text type that has no charset, and it rejects
an array for that header.
res.set('Cache-Control', 'no-store');
res.set({ 'X-Request-Id': req.id, 'Content-Language': 'en' });
res.get('content-language'); // 'en'
Because set() replaces, calling it for a header that several layers contribute to (Vary, Link, Set-Cookie) loses
what an earlier layer added. Use the helpers designed for accumulation: res.append(), res.vary(), res.links() and
res.cookie().
res.append()
res.append(field, value) adds a value to a header, creating the header when it does not exist, and accepts a string or an
array. A later res.set() for the same field overwrites everything that was appended, so order matters:
res.append('Link', ['<https://api.bookshelf.example/books?page=2>; rel="next"']);
res.append('Set-Cookie', 'theme=dark; Path=/; HttpOnly; SameSite=Lax');
For cookies prefer res.cookie(), which serializes and escapes the value for you.
res.type()
res.type(type) (alias res.contentType()) sets Content-Type. An argument that contains a slash is used as the MIME type
as it is; anything else is looked up as a file extension through the mime-types package, with or without the leading
dot:
res.type('.html'); // text/html; charset=utf-8
res.type('json'); // application/json; charset=utf-8
res.type('application/pdf'); // application/pdf
Source: Express API: Response (res.type)
res.send() only chooses a default Content-Type when none is set, so calling res.type() first is the way to send a
string that is not HTML.
res.vary()
res.vary(field) adds a field to the Vary header unless it is already listed, which tells caches that the representation
depends on that request header. Any response whose body depends on Accept, Accept-Language, Accept-Encoding or
Origin must say so, otherwise a shared cache can serve one client’s variant to another. Express 5 throws a TypeError
when res.vary() is called without a field (Express 4 ignored the call). res.format() and the cors middleware call it
for you.
res.links()
res.links(object) builds the Link header from an object whose keys are relation names and whose values are a URL or an
array of URLs. Calls append to an existing Link header. It is the right tool for pagination and discovery:
res.links({
prev: 'https://api.bookshelf.example/books?page=1',
next: 'https://api.bookshelf.example/books?page=3',
last: ['https://api.bookshelf.example/books?page=9'],
});
// Link: <https://api.bookshelf.example/books?page=1>; rel="prev", ...
The helper does not percent-encode or validate the URLs it receives, so build them from trusted pieces.
res.location()
res.location(url) sets the Location header and returns res. It encodes characters that are not valid in a URL but it
does not check where the URL points; see Open-redirect safety. Express 5 removed the special value 'back'; read the
Referer header (or better, an explicit parameter) yourself.
res.attachment()
res.attachment([filename]) sets Content-Disposition: attachment. With a file name it also sets Content-Type from the
extension and adds the filename parameter (and a filename* parameter for non-ASCII names). It sends nothing by itself;
combine it with res.send() or a stream. res.download() does both steps for a file on disk.
res.attachment('books.csv');
res.send('id,title\n1,Dune\n');
Sending a body: res.send()
res.send([body]) is the general-purpose terminal helper. It sets Content-Length, handles HEAD, builds an ETag when
the etag application setting allows it, answers 304 Not Modified when the request is fresh, and chooses a
Content-Type from the type of body when one is not already set:
| Argument | Default Content-Type |
Notes |
|---|---|---|
|
|
Call |
|
|
Used as it is; the type of a Buffer is never guessed. |
|
|
Delegated to |
|
none |
Ends the response with an empty body. Use |
Some details worth remembering:
-
A
204or304response has its body and entity headers removed even if you pass a body. -
A stream is not accepted by
res.send(). Pipe it (stream.pipeline(readable, res)) or useres.sendFile(). -
Strings are sent as UTF-8 by default. An
ETagis generated for non-empty bodies only. -
res.send()can be called once. A second call raisesERR_HTTP_HEADERS_SENT(res.headersSentand the double-response bug).
JSON: res.json() and the json * settings
res.json([body]) serializes body with JSON.stringify(), sets Content-Type: application/json; charset=utf-8 unless
one is already set, and sends the result through res.send(). It accepts any JSON value, including null, arrays,
strings, numbers and booleans. Three application settings change how the serialization happens; they are described in
Application and Settings and apply to res.json() and res.jsonp():
| Setting | Effect | Typical use |
|---|---|---|
|
The second argument of |
Dropping |
|
The third argument: indentation. Off by default. |
Readable output in development. Do not enable it in production, where it only adds bytes. |
|
When |
Defence in depth when a JSON response could be sniffed or embedded as HTML. |
JSON.stringify() calls toJSON() on values that define it (so a Date becomes an ISO string and a Mongoose document
can control its own shape), and it throws on circular structures and on BigInt. A thrown error inside an async handler
is forwarded to the error handler in Express 5; in a callback it is not.
Never return a model or an ORM entity directly. Map it to a response shape that lists exactly the fields clients may see; that single habit prevents leaking password hashes, internal ids and flags, a mistake several of the books make.
res.jsonp()
res.jsonp([body]) behaves like res.json() but wraps the result in a function call when the request carries a callback
name in its query string. The parameter name is callback by default and can be changed with the jsonp callback name
setting. Express restricts the name to characters that cannot break out of the call, removes anything else, prefixes the body
with a comment, and sets X-Content-Type-Options: nosniff, but JSONP is still a legacy mechanism: it executes the response
as script in the calling page, has no error reporting and cannot be protected by CORS. Use it only to support an existing
client that cannot be changed; new code should use res.json() with the cors middleware (see
CORS).
app.set('jsonp callback name', 'cb');
// GET /legacy/books?cb=render -> /**/ typeof render === 'function' && render([...]);
app.get('/legacy/books', (req, res) => res.jsonp([{ id: '1', title: 'Dune' }]));
Redirects: res.redirect()
res.redirect([status], url) sets the Location header (through res.location()), sets the status (302 Found unless you
pass one), and sends a short body that is negotiated against Accept (a small HTML page with a link, or the text
Found. Redirecting to <url>, or an empty body for HEAD). The URL may be absolute (https://example.com), root-relative
(/login), or relative to the current URL (post/new, ..); relative targets resolve the way a browser resolves them, so
the trailing slash on the current path decides the result: from /blog/admin/, post/new leads to /blog/admin/post/new,
and from /blog/admin to /blog/post/new.
Choose the status deliberately:
| Status | Use |
|---|---|
|
Permanent move. |
|
Temporary redirect. |
|
Redirect to a |
Express 5 removed the res.redirect(url, status) argument order and the magic 'back' URL (see
Removed and changed in Express 5).
Open-redirect safety
Both res.redirect() and res.location() encode the URL but never validate it, so passing a value taken from req.query
or req.body produces an open redirect: an attacker sends a victim a link on your domain that bounces to a phishing page.
Accept only destinations you can check: a fixed map of names, or a relative path that starts with a single /, is not
protocol-relative (//evil.example) and does not contain a backslash or control characters. When an absolute URL is
needed, parse it with new URL() and compare the origin against an allow-list.
// redirect-safety.mjs
import express from 'express';
const app = express();
const allowedOrigins = new Set(['https://bookshelf.example', 'https://admin.bookshelf.example']);
function safeRedirectTarget(value, fallback = '/') {
if (typeof value !== 'string' || value.length === 0 || value.length > 2048) {
return fallback;
}
// Same-site relative path: exactly one leading slash, no backslash, no control characters.
if (value.startsWith('/') && !value.startsWith('//') && !/[\\\u0000-\u001f]/.test(value)) {
return value;
}
try {
const url = new URL(value);
return allowedOrigins.has(url.origin) ? url.href : fallback;
} catch {
return fallback;
}
}
app.get('/login/done', (req, res) => {
res.redirect(303, safeRedirectTarget(req.query.next, '/books'));
});
app.listen(3000, (error) => {
if (error) {
throw error;
}
console.log('Listening on http://localhost:3000');
});
Source: https://expressjs.com/en/api/response/ and https://expressjs.com/en/advanced/best-practice-security/ (prevent open redirects).
Templates: res.render()
res.render(view, [locals], [callback]) renders a view with the configured template engine and sends the result as HTML.
view is a file name relative to the views setting (or an absolute path); if it has no extension, the view engine
setting supplies one, and if it has one, Express loads the matching engine module and calls its __express function. The
variables available to the template are the merge of app.locals, res.locals and the locals argument, in that order of
increasing priority.
-
Without a callback, Express sends the HTML with status
200(set a different status first withres.status()), and a render error goes tonext(err). -
With a callback,
callback(err, html)receives the result and nothing is sent automatically, so the handler must send the HTML or forward the error. This is how to render e-mail bodies or fragments. -
In Express 5 rendering is always asynchronous, even for an engine that is synchronous or a cached view, so code that relied on the callback running immediately must be adapted.
-
View caching is on in production (
view cache) and off elsewhere. -
Never build
viewfrom user input, and never copyreq.bodyorreq.querywholesale intolocals: both open the door to file inclusion and to overwriting options such assettings. Escape output in templates.
res.status(404).render('errors/not-found', { title: 'Not found', path: req.path });
Template engines, layouts and escaping are covered in Template Engines and Views.
Files: res.sendFile() and res.download()
res.sendFile()
res.sendFile(path, [options], [callback]) streams a file and sets Content-Type from its extension, Content-Length,
Last-Modified, an ETag, Cache-Control and Accept-Ranges. It supports conditional requests (304) and byte ranges
(206), so media can be seeked. The work is done by the send package, the same one behind express.static().
The path must be absolute unless the root option is given; otherwise Express throws. Relative paths may contain .. only
when the result stays inside root.
| Option | Default | Meaning |
|---|---|---|
|
none |
Directory against which a relative |
|
|
|
|
|
Adds |
|
|
Set to |
|
|
Set |
|
none |
Object of extra headers sent with the file. |
|
|
|
|
|
Allow |
The callback is called with an error when the transfer fails (a missing file has err.status === 404, an aborted download
has code ECONNABORTED) and with no argument on success. When you supply a callback, Express no longer finishes the failed
response for you: you must send an error response, call next(err), or end the response. Check res.headersSent first,
because the transfer may already be under way.
// covers.mjs
import { join } from 'node:path';
import express from 'express';
const app = express();
const coversDirectory = join(import.meta.dirname, 'covers');
app.get('/books/:id/cover', (req, res, next) => {
// The :id pattern is restricted, and root keeps the lookup inside covers/.
if (!/^[\w-]{1,40}$/.test(req.params.id)) {
return res.status(400).json({ error: 'invalid id' });
}
res.sendFile(`${req.params.id}.png`, {
root: coversDirectory,
dotfiles: 'deny',
maxAge: '1h',
headers: { 'X-Content-Type-Options': 'nosniff' },
}, (error) => {
if (error && !res.headersSent) {
next(error.status === 404 ? undefined : error);
}
});
});
app.listen(3000);
Source: https://expressjs.com/en/api/response/ (res.sendFile).
res.download()
res.download(path, [filename], [options], [callback]) transfers the file as an attachment: it calls res.attachment() to
set Content-Disposition, then res.sendFile(). The optional filename overrides the name shown in the browser’s save
dialog, options are those of sendFile, and the callback has the same contract (end the response or call next, and
check res.headersSent). The same path-safety rules apply: use root when the path derives from user input.
res.download(join(exportsDirectory, 'books-2026.csv'), 'bookshelf-books.csv', (error) => {
if (error && !res.headersSent) {
next(error);
}
});
Large downloads, uploads and streaming are covered in File Uploads and Downloads.
Content negotiation: res.format()
res.format(object) picks one handler according to the request’s Accept header using req.accepts(), honouring quality
values. The keys are MIME types or extension shortcuts (text, html, json); an optional default key handles everything
else. It sets Vary: Accept and the Content-Type of the chosen type automatically (the handler may override it). If there
is no Accept header the first handler runs; if nothing matches and there is no default, Express passes a 406 Not
Acceptable error to next().
// negotiation.mjs
import express from 'express';
const app = express();
const book = { id: '1', title: 'Dune', author: 'Frank Herbert' };
app.get('/books/1', (req, res) => {
res.format({
text() {
res.send(`${book.title} by ${book.author}`);
},
html() {
res.send(`<h1>${book.title.replace(/[&<>]/g, '')}</h1>`);
},
json() {
res.json(book);
},
default() {
res.status(406).type('text').send('Not Acceptable');
},
});
});
app.listen(3000);
Source: https://expressjs.com/en/api/response/ (res.format). Quality values, Vary and caching are explained further in
Content Negotiation and HTTP Caching.
Cookies: res.cookie() and res.clearCookie()
res.cookie(name, value, [options]) appends a Set-Cookie header and returns res, so several cookies can be chained.
It only sets a header: nothing is stored on the server. A value that is an object is serialized as JSON, and the value is
escaped with encodeURIComponent unless you supply another synchronous encode function. Options that you leave out take
the defaults of the RFC 6265 attribute rules, which is why a cookie without options is a session cookie visible to
scripts and sent over plain HTTP.
| Option | Type | Meaning |
|---|---|---|
|
|
Domain the cookie is sent to. Omit it for a host-only cookie, which is safer than a domain cookie shared by subdomains. |
|
|
Path prefix of requests that carry the cookie. |
|
|
Absolute expiry in GMT. Without |
|
|
Lifetime in milliseconds from now (the cookie header carries seconds). Takes precedence in browsers when both are present. |
|
|
Hides the cookie from |
|
|
Send only over HTTPS. Required for |
|
|
Appends a signature so that tampering is detected. Needs |
|
|
|
|
|
Adds the |
|
|
Value of the |
|
|
Synchronous encoder, default |
// cookies.mjs
import express from 'express';
import cookieParser from 'cookie-parser';
const app = express();
const cookieSecret = process.env.COOKIE_SECRET;
if (!cookieSecret) throw new Error('COOKIE_SECRET is not set');
app.use(cookieParser(cookieSecret));
app.post('/preferences', express.json(), (req, res) => {
const theme = req.body?.theme === 'dark' ? 'dark' : 'light';
res
.status(204)
.cookie('theme', theme, {
httpOnly: true,
secure: app.get('env') === 'production',
sameSite: 'lax',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days, in milliseconds
signed: true,
priority: 'low',
})
.end();
});
// A cookie embedded in a third-party page, kept per top-level site.
app.get('/widget', (req, res) => {
res.cookie('widget_state', 'compact', {
httpOnly: true,
secure: true,
sameSite: 'none',
partitioned: true,
path: '/widget',
}).send('ok');
});
app.post('/preferences/reset', (req, res) => {
// The options must match the ones used to set the cookie, apart from expires and maxAge.
res.clearCookie('theme', { httpOnly: true, secure: app.get('env') === 'production', sameSite: 'lax' });
res.sendStatus(204);
});
app.listen(3000);
Source: https://expressjs.com/en/api/response/ (res.cookie, res.clearCookie).
res.clearCookie(name, [options]) sends an expired cookie. A browser removes a cookie only when the name, domain and
path (and, for partitioned cookies, the partition) match the original, so pass the same options you set. Express 5 ignores
expires and maxAge here: they are forced to an expiry in the past, where Express 4 let a caller override them and
accidentally keep the cookie alive. Cookie parsing, sessions, cookie-session and the choice between them live in
Cookies and Sessions.
res.headersSent and the double-response bug
Once the status line and headers have been flushed, they cannot change. res.headersSent is true from that moment, and
trying to set a header or send again raises ERR_HTTP_HEADERS_SENT. The usual causes:
-
a handler that sends a response in an
ifbranch and forgets toreturn, so the code continues and sends again; -
both a callback and a promise path answering;
-
an error that happens after a partial response (a failed
sendFile, a thrown error while streaming); -
an error-handling middleware that tries to send a JSON error after the response has begun.
An error-handling middleware must check the flag and delegate to Express when the response has started, because the default handler then closes the connection, which is the only correct thing left to do:
app.use((error, req, res, next) => {
if (res.headersSent) {
return next(error);
}
res.status(error.status ?? 500).json({ error: error.expose ? error.message : 'Internal Server Error' });
});
Removed and changed in Express 5
Most "it worked in Express 4" failures on this object come from signatures that were deprecated for years and removed in
Express 5.0. Every removed form has a one-line replacement, and the official codemods (for example
npx codemod@latest @expressjs/v5-migration-recipe) rewrite most of them. Migration as a whole is covered in
Migrating and What’s Changed.
| Express 4 code | Express 5 | Replacement |
|---|---|---|
|
Removed |
|
|
Removed |
|
|
Removed |
|
|
Removed; a number is sent as a JSON body |
|
|
Removed |
|
|
Removed |
Redirect to an explicit, validated URL; if you must use the referrer, read |
|
Removed |
|
|
Removed |
|
|
|
Drop them; match |
|
Throws unless an integer from 100 to 999 |
Pass a number |
|
Throws |
Always pass a header name |
|
Always asynchronous |
Do not rely on timing |
Two related behaviours changed outside res: req.body is undefined when no body parser ran, and rejected promises
returned from handlers are forwarded to the error handler, which removes the need for a try/catch around every await
that only calls next(error).
A complete example: a Bookshelf response toolkit
The following program uses most of the helpers together: a created resource with Location, pagination with Link,
negotiated output, an attachment, safe redirect, and a final error handler that respects headersSent. Run it with
npm install express and node response-demo.mjs.
// response-demo.mjs
import { randomUUID } from 'node:crypto';
import express from 'express';
const app = express();
app.set('etag', 'strong');
app.set('json spaces', app.get('env') === 'production' ? 0 : 2);
app.disable('x-powered-by');
const books = new Map([
['1', { id: '1', title: 'Dune', author: 'Frank Herbert' }],
['2', { id: '2', title: 'Hyperion', author: 'Dan Simmons' }],
]);
// Response shape: list exactly the fields clients may see.
const toDto = ({ id, title, author }) => ({ id, title, author });
app.use(express.json({ limit: '50kb' }));
app.get('/books', (req, res) => {
const page = Math.max(1, Number.parseInt(req.query.page ?? '1', 10) || 1);
const size = 1;
const items = [...books.values()].slice((page - 1) * size, page * size).map(toDto);
const last = Math.max(1, Math.ceil(books.size / size));
const base = `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}`;
const links = { first: `${base}?page=1`, last: `${base}?page=${last}` };
if (page > 1) links.prev = `${base}?page=${page - 1}`;
if (page < last) links.next = `${base}?page=${page + 1}`;
res.links(links).set('X-Total-Count', String(books.size)).json(items);
});
app.post('/books', (req, res) => {
const { title, author } = req.body ?? {};
if (typeof title !== 'string' || typeof author !== 'string') {
return res.status(422).json({ error: 'title and author are required' });
}
const book = { id: randomUUID(), title, author };
books.set(book.id, book);
res.status(201).location(`/books/${book.id}`).json(toDto(book));
});
app.get('/books/export.csv', (req, res) => {
const rows = ['id,title,author', ...[...books.values()].map((b) => `${b.id},"${b.title}","${b.author}"`)];
res.attachment('books.csv').type('csv').send(`${rows.join('\n')}\n`);
});
app.get('/books/:id', (req, res) => {
const book = books.get(req.params.id);
if (!book) {
return res.status(404).json({ error: 'book not found' });
}
res.format({
json: () => res.json(toDto(book)),
text: () => res.type('text').send(`${book.title} by ${book.author}\n`),
default: () => res.status(406).type('text').send('Not Acceptable\n'),
});
});
app.delete('/books/:id', (req, res) => {
books.delete(req.params.id);
res.status(204).end();
});
app.get('/old-books/:id', (req, res) => {
res.redirect(308, `/books/${encodeURIComponent(req.params.id)}`);
});
app.use((req, res) => {
res.status(404).json({ error: 'not found' });
});
app.use((error, req, res, next) => {
if (res.headersSent) {
return next(error);
}
res.status(error.status ?? 500).json({ error: error.expose ? error.message : 'Internal Server Error' });
});
app.listen(3000, (error) => {
if (error) {
throw error;
}
console.log('Bookshelf listening on http://localhost:3000');
});
Source: https://expressjs.com/en/api/response/ and https://expressjs.com/en/guide/routing/ (response methods).
Common pitfalls
-
Sending twice (
res.headersSentand the double-response bug): alwaysreturnwhen you respond early. -
Calling
res.status()afterres.send(); the status is applied before the body, so the order must beres.status(404).json(body). -
Setting a header after the body started (for example
res.set()inside afinishlistener): too late. -
Returning a database document directly from
res.json(): leaks fields; map to a DTO. -
Passing user input to
res.redirect(),res.location(),res.render()(as the view name),res.sendFile()(withoutroot) orres.download(). -
Using
res.send()with a string that is actually JSON: it is labelledtext/html. Useres.json()orres.type('json'). -
Forgetting
Varywhen the body depends on a request header. -
Using
res.cookie()withsameSite: 'none'withoutsecure, which browsers reject. -
Forgetting that
res.clearCookie()needs the samepathanddomainas the cookie it removes. -
Calling
res.sendFile()with a relative path and noroot: Express throws.
Where the books are outdated
The four books consulted for this section target Express 4.0 to 4.18 and Node.js 0.10 to 20. Their treatment of the response object is accurate as a picture of the concepts, but these practices have since changed:
| Older practice | Where it appears | Current behaviour |
|---|---|---|
Status-first call signatures such as |
Sharma, chapters 2 and 4; Monteiro 2014, chapter 3 |
Removed in Express 5. Chain |
|
Monteiro 2014, chapter 3 |
|
|
Monteiro 2016, chapter 4 |
Removed. Redirect to an explicit and validated URL. |
|
Monteiro 2014, chapter 4; Monteiro 2016, chapter 3 |
Replaced by |
Returning the whole Mongoose document, including hashes, with |
Sharma, chapter 6 |
Return a response shape with an explicit field list; configure |
Error responses written as |
Monteiro 2014 and 2016; Sharma |
One error-handling middleware that sets the status and a safe message, and checks |
Session or JWT cookies set without |
Sharma, chapter 6; Monteiro 2016, chapter 6 |
Always set |
|
Monteiro 2014, chapter 3; Monteiro 2016, chapter 1 |
Rendering is asynchronous in Express 5; Jade is now Pug. |
References
-
Related pages: Request, Routing, Middleware Fundamentals, Content Negotiation and HTTP Caching, Error Handling