diff --git a/docs/content.md b/docs/content.md
index 78ef752884..166fd0be55 100644
--- a/docs/content.md
+++ b/docs/content.md
@@ -134,6 +134,20 @@ src/content/api/
- Versioned: `/en/5x/api/application` → Express 5.x application API
- Non-versioned: `/en/api/application` → Defaults to Express 5.x
+### Style Conventions
+
+The reference pages for every version follow the same conventions, so that switching between
+Express 4.x and 5.x only shows real API differences:
+
+- Property and method sections are listed alphabetically, so `req.host` comes before `req.hostname`.
+- Result comments in code samples use single quotes, matching the Prettier `singleQuote` setting:
+ `res.type('png'); // => 'image/png'`. Double quotes are kept only where the sample shows JSON or a
+ raw HTTP header.
+- The frontmatter `description` of the request and response pages repeats the opening sentence of the
+ page, which refers to the objects as `req` and `res` rather than spelling out their full names.
+
+`tests/unit/api-docs-style.test.mjs` checks these conventions, and it runs as part of `npm run test:unit`.
+
## Versioning
The Express.js documentation supports multiple versions. Content is organized by version in both the `docs` and `api` collections.
diff --git a/src/content/api/4x/api/application/index.mdx b/src/content/api/4x/api/application/index.mdx
index 0288d17b0a..bc075745a6 100644
--- a/src/content/api/4x/api/application/index.mdx
+++ b/src/content/api/4x/api/application/index.mdx
@@ -560,7 +560,7 @@ app.get('title');
app.set('title', 'My Site');
app.get('title');
-// => "My Site"
+// => 'My Site'
```
### app.get()
diff --git a/src/content/api/4x/api/request/index.mdx b/src/content/api/4x/api/request/index.mdx
index 6ebc99ff55..75dc5f26c6 100644
--- a/src/content/api/4x/api/request/index.mdx
+++ b/src/content/api/4x/api/request/index.mdx
@@ -244,6 +244,22 @@ console.dir(req.fresh);
// => true
```
+### req.host
+
+
+
+
+
+`req.host` is a deprecated alias of [`req.hostname`](#reqhostname) and returns the same value — the hostname with the port number stripped off. It has emitted a deprecation warning since Express v4.5.0. Use `req.hostname` instead.
+
+
+
+```js
+// Host: "example.com:3000"
+console.dir(req.host);
+// => 'example.com'
+```
+
### req.hostname
@@ -272,22 +288,6 @@ console.dir(req.hostname);
// => 'example.com'
```
-### req.host
-
-
-
-
-
-`req.host` is a deprecated alias of [`req.hostname`](#reqhostname) and returns the same value — the hostname with the port number stripped off. It has emitted a deprecation warning since Express v4.5.0. Use `req.hostname` instead.
-
-
-
-```js
-// Host: "example.com:3000"
-console.dir(req.host);
-// => 'example.com'
-```
-
### req.ip
@@ -671,17 +671,17 @@ list or array, the method returns the _best_ match (if any).
```js
// Accept: text/html
req.accepts('html');
-// => "html"
+// => 'html'
// Accept: text/*, application/json
req.accepts('html');
-// => "html"
+// => 'html'
req.accepts('text/html');
-// => "text/html"
+// => 'text/html'
req.accepts(['json', 'text']);
-// => "json"
+// => 'json'
req.accepts('application/json');
-// => "application/json"
+// => 'application/json'
// Accept: text/*, application/json
req.accepts('image/png');
@@ -690,7 +690,7 @@ req.accepts('png');
// Accept: text/*;q=.5, application/json
req.accepts(['html', 'json']);
-// => "json"
+// => 'json'
```
For more information, or if you have issues or concerns, see [accepts](https://github.com/expressjs/accepts).
@@ -856,10 +856,10 @@ The `Referrer` and `Referer` fields are interchangeable.
```js
req.get('Content-Type');
-// => "text/plain"
+// => 'text/plain'
req.get('content-type');
-// => "text/plain"
+// => 'text/plain'
req.get('Something');
// => undefined
@@ -943,15 +943,15 @@ Returns the value of param `name` when present.
```js
// ?name=tobi
req.param('name');
-// => "tobi"
+// => 'tobi'
// POST name=tobi
req.param('name');
-// => "tobi"
+// => 'tobi'
// /user/tobi for /user/:name
req.param('name');
-// => "tobi"
+// => 'tobi'
```
Lookup is performed in the following order:
diff --git a/src/content/api/4x/api/response/index.mdx b/src/content/api/4x/api/response/index.mdx
index 75a8eb52dc..1b78c454bb 100644
--- a/src/content/api/4x/api/response/index.mdx
+++ b/src/content/api/4x/api/response/index.mdx
@@ -572,7 +572,7 @@ The match is case-insensitive.
```js
res.get('Content-Type');
-// => "text/plain"
+// => 'text/plain'
```
### res.json()
diff --git a/src/content/api/5x/api/application/index.mdx b/src/content/api/5x/api/application/index.mdx
index 31ef5348e7..fcbb181455 100644
--- a/src/content/api/5x/api/application/index.mdx
+++ b/src/content/api/5x/api/application/index.mdx
@@ -585,7 +585,7 @@ app.get('title');
app.set('title', 'My Site');
app.get('title');
-// => "My Site"
+// => 'My Site'
```
### app.get()
diff --git a/src/content/api/5x/api/request/index.mdx b/src/content/api/5x/api/request/index.mdx
index 0340d3cd95..3799d35959 100644
--- a/src/content/api/5x/api/request/index.mdx
+++ b/src/content/api/5x/api/request/index.mdx
@@ -1,6 +1,6 @@
---
title: Request Object
-description: The request object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on
+description: The req object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on
---
import Alert from '@components/primitives/Alert/Alert.astro';
@@ -211,7 +211,7 @@ contains cookies sent by the request. If the request contains no cookies, it def
```js
// Cookie: name=tj
console.dir(req.cookies.name);
-// => "tj"
+// => 'tj'
```
If the cookie has been signed, you have to use [req.signedCookies](#reqsignedcookies).
@@ -292,7 +292,7 @@ the value of this property is derived from the left-most entry in the
```js
console.dir(req.ip);
-// => "127.0.0.1"
+// => '127.0.0.1'
```
### req.ips
@@ -361,7 +361,7 @@ the "mounting" feature of [app.use()](/api/application/#appuse) will rewrite `re
```js
// GET /search?q=something
console.dir(req.originalUrl);
-// => "/search?q=something"
+// => '/search?q=something'
```
`req.originalUrl` is available both in middleware and router objects, and is a
@@ -398,7 +398,7 @@ This property is an object containing properties mapped to the [named route "par
```js
// GET /user/tj
console.dir(req.params.name);
-// => "tj"
+// => 'tj'
```
Properties corresponding to wildcard parameters are arrays containing separate path segments split on `/`:
@@ -433,7 +433,7 @@ When you use a regular expression for the route definition, capture groups are p
app.use(/^\/file\/(.*)$/, (req, res) => {
// GET /file/javascripts/jquery.js
console.dir(req.params[0]);
- // => "javascripts/jquery.js"
+ // => 'javascripts/jquery.js'
});
```
@@ -443,7 +443,7 @@ import { type Request, type Response } from 'express';
app.use(/^\/file\/(.*)$/, (req: Request, res: Response) => {
// GET /file/javascripts/jquery.js
console.dir(req.params[0]);
- // => "javascripts/jquery.js"
+ // => 'javascripts/jquery.js'
});
```
@@ -468,7 +468,7 @@ Contains the path part of the request URL.
```js
// example.com/users?sort=desc
console.dir(req.path);
-// => "/users"
+// => '/users'
```
@@ -490,7 +490,7 @@ This header can be set by the client or by the proxy.
```js
console.dir(req.protocol);
-// => "http"
+// => 'http'
```
### req.query
@@ -637,7 +637,7 @@ If no signed cookies are sent, the property defaults to `{}`.
```js
// Cookie: user=tobi.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3
console.dir(req.signedCookies.user);
-// => "tobi"
+// => 'tobi'
```
For more information, issues, or concerns, see [cookie-parser](https://github.com/expressjs/cookie-parser).
@@ -663,7 +663,7 @@ An array of subdomains in the domain name of the request.
```js
// Host: "tobi.ferrets.example.com"
console.dir(req.subdomains);
-// => ["ferrets", "tobi"]
+// => ['ferrets', 'tobi']
```
The application property `subdomain offset`, which defaults to 2, is used for determining the
@@ -708,17 +708,17 @@ list or array, the method returns the _best_ match (if any).
```js
// Accept: text/html
req.accepts('html');
-// => "html"
+// => 'html'
// Accept: text/*, application/json
req.accepts('html');
-// => "html"
+// => 'html'
req.accepts('text/html');
-// => "text/html"
+// => 'text/html'
req.accepts(['json', 'text']);
-// => "json"
+// => 'json'
req.accepts('application/json');
-// => "application/json"
+// => 'application/json'
// Accept: text/*, application/json
req.accepts('image/png');
@@ -727,7 +727,7 @@ req.accepts('png');
// Accept: text/*;q=.5, application/json
req.accepts(['html', 'json']);
-// => "json"
+// => 'json'
```
For more information, or if you have issues or concerns, see [accepts](https://github.com/expressjs/accepts).
@@ -832,10 +832,10 @@ The `Referrer` and `Referer` fields are interchangeable.
```js
req.get('Content-Type');
-// => "text/plain"
+// => 'text/plain'
req.get('content-type');
-// => "text/plain"
+// => 'text/plain'
req.get('Something');
// => undefined
diff --git a/src/content/api/5x/api/response/index.mdx b/src/content/api/5x/api/response/index.mdx
index 52b3d287f4..a582142d1f 100644
--- a/src/content/api/5x/api/response/index.mdx
+++ b/src/content/api/5x/api/response/index.mdx
@@ -1,6 +1,6 @@
---
title: Response
-description: The response object represents the HTTP response that an Express app sends when it gets an HTTP request.
+description: The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
---
import Alert from '@components/primitives/Alert/Alert.astro';
@@ -564,7 +564,7 @@ The match is case-insensitive.
```js
res.get('Content-Type');
-// => "text/plain"
+// => 'text/plain'
```
### res.json()
@@ -1104,7 +1104,7 @@ res.type('.html'); // => 'text/html'
res.type('html'); // => 'text/html'
res.type('json'); // => 'application/json'
res.type('application/json'); // => 'application/json'
-res.type('png'); // => image/png:
+res.type('png'); // => 'image/png'
```
Aliased as `res.contentType(type)`.
diff --git a/tests/unit/api-docs-style.test.mjs b/tests/unit/api-docs-style.test.mjs
new file mode 100644
index 0000000000..5aa1f71683
--- /dev/null
+++ b/tests/unit/api-docs-style.test.mjs
@@ -0,0 +1,73 @@
+import assert from 'node:assert/strict';
+import { readFileSync, readdirSync } from 'node:fs';
+import { dirname, join, relative, resolve } from 'node:path';
+import { test } from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
+const API_ROOT = join(ROOT, 'src', 'content', 'api');
+const VERSIONS = ['4x', '5x'];
+
+function collectMdx(dir) {
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const full = join(dir, entry.name);
+ if (entry.isDirectory()) return collectMdx(full);
+ return entry.name.endsWith('.mdx') ? [full] : [];
+ });
+}
+
+function findOffendingLines(isOffending) {
+ const offenders = [];
+ for (const file of VERSIONS.flatMap((version) => collectMdx(join(API_ROOT, version)))) {
+ readFileSync(file, 'utf8')
+ .split('\n')
+ .forEach((line, index) => {
+ if (isOffending(line)) {
+ offenders.push(`${relative(ROOT, file)}:${index + 1} ${line.trim()}`);
+ }
+ });
+ }
+ return offenders;
+}
+
+test('result comments quote string values with single quotes', () => {
+ const offenders = findOffendingLines(
+ (line) => /^\s*\/\/\s*=>.*"/.test(line) && !line.includes('{')
+ );
+ assert.deepEqual(offenders, []);
+});
+
+test('result comments do not end with stray punctuation', () => {
+ const offenders = findOffendingLines((line) => /^\s*\/\/\s*=>.*[:;,]\s*$/.test(line));
+ assert.deepEqual(offenders, []);
+});
+
+test('req.host is documented before req.hostname', () => {
+ for (const version of VERSIONS) {
+ const file = join(API_ROOT, version, 'api', 'request', 'index.mdx');
+ const content = readFileSync(file, 'utf8');
+ const host = content.indexOf('### req.host\n');
+ const hostname = content.indexOf('### req.hostname\n');
+ assert.notEqual(host, -1, `req.host heading is missing in ${version}`);
+ assert.notEqual(hostname, -1, `req.hostname heading is missing in ${version}`);
+ assert.ok(host < hostname, `req.host should precede req.hostname in ${version}`);
+ }
+});
+
+test('request and response pages describe themselves as req and res', () => {
+ const pages = [
+ ['request', 'req'],
+ ['response', 'res'],
+ ];
+ for (const version of VERSIONS) {
+ for (const [page, name] of pages) {
+ const file = join(API_ROOT, version, 'api', page, 'index.mdx');
+ const description = readFileSync(file, 'utf8').match(/^description: (.*)$/m);
+ assert.ok(description, `${version}/${page} has no description`);
+ assert.ok(
+ description[1].startsWith(`The ${name} object represents`),
+ `${version}/${page} description should start with 'The ${name} object represents'`
+ );
+ }
+ }
+});