diff --git a/.github/scripts/get-readmes.sh b/.github/scripts/get-readmes.sh index 26ad390f82..5b04ee067f 100755 --- a/.github/scripts/get-readmes.sh +++ b/.github/scripts/get-readmes.sh @@ -52,6 +52,34 @@ LIST_END VERSION=$(echo "$NPM_DATA" | jq -r '.["dist-tags"].latest // empty') DESC=$(echo "$NPM_DATA" | jq -r '.description // empty') + # Build a TypeScript types note for this package: + # - If the latest release declares its own types (a "types"/"typings" field), + # it ships types and no separate install is needed. + # - Otherwise, if a community-maintained @types/ package exists on npm, + # recommend installing it as a dev dependency. + # This relies on the declared types field; a package that ships a co-located + # index.d.ts without that field would not be detected (none currently do). + BUNDLED_TYPES=$(echo "$NPM_DATA" | jq -r --arg v "$VERSION" '(.versions[$v].types // .versions[$v].typings) // empty') + ATTYPES_VERSION=$(curl -s "https://registry.npmjs.org/@types/$NPM_PKG" | jq -r '.["dist-tags"].latest // empty') + DT_URL="https://github.com/DefinitelyTyped/DefinitelyTyped" + if [ -n "$BUNDLED_TYPES" ]; then + TYPES_ALERT=" + +\`$NPM_PKG\` ships its own TypeScript type definitions, so you do not need to install a separate \`@types\` package. + +" + elif [ -n "$ATTYPES_VERSION" ]; then + TYPES_ALERT=" + +\`$NPM_PKG\` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](${DT_URL}) as a development dependency: + + + +" + else + TYPES_ALERT="" + fi + # Preserve existing frontmatter if the file already exists FRONTMATTER="" if [ -f "$DEST" ]; then @@ -118,6 +146,14 @@ LIST_END } }ge') + # Insert the TypeScript types note right after the install command (the first + # `npm install`/`npm i` PackageManagerCommand). Left untouched if none is found. + if [ -n "$TYPES_ALERT" ]; then + CONTENT=$(TYPES_ALERT="$TYPES_ALERT" perl -0777 -pe ' + s{()}{$1\n\n$ENV{TYPES_ALERT}}; + ' <<<"$CONTENT") + fi + # Convert relative links to absolute GitHub URLs BASEURL="https://github.com/$org/$repo/blob/HEAD" CONTENT=$(echo "$CONTENT" | sed -E "s|\]\(([^)#/][^):]*)\)|](${BASEURL}/\1)|g") diff --git a/src/content/api/4x/api.mdx b/src/content/api/4x/api.mdx index 5f2343d622..8d2cfd327d 100644 --- a/src/content/api/4x/api.mdx +++ b/src/content/api/4x/api.mdx @@ -33,6 +33,18 @@ app.get('/', function (req, res) { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', function (req: Request, res: Response) { + res.send('hello world'); +}); + +app.listen(3000); +``` + Express 4.x requires **Node.js 0.10.0 or higher**. diff --git a/src/content/api/4x/api/application/index.mdx b/src/content/api/4x/api/application/index.mdx index 98ee878851..ebac8720b3 100644 --- a/src/content/api/4x/api/application/index.mdx +++ b/src/content/api/4x/api/application/index.mdx @@ -34,6 +34,18 @@ app.get('/', function (req, res) { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', function (req: Request, res: Response) { + res.send('hello world'); +}); + +app.listen(3000); +``` + The `app` object has methods for - Routing HTTP requests; see for example, [app.METHOD](#appmethod) and [app.param](#appparam). @@ -130,6 +142,20 @@ admin.get('/', function (req, res) { app.use('/admin', admin); // mount the sub app ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); // the main app +const admin: Express = express(); // the sub app + +admin.get('/', function (req: Request, res: Response) { + console.log(admin.mountpath); // /admin + res.send('Admin Homepage'); +}); + +app.use('/admin', admin); // mount the sub app +``` + It is similar to the [baseUrl](/api/request/#reqbaseurl) property of the `req` object, except `req.baseUrl` returns the matched URL path, instead of the matched patterns. @@ -154,6 +180,26 @@ admin.use('/secr*t', secret); // load the 'secret' router on '/secr*t', on the ' app.use(['/adm*n', '/manager'], admin); // load the 'admin' router on '/adm*n' and '/manager', on the parent app ``` +```ts +import express, { type Express, type Request, type Response } from 'express'; + +const admin: Express = express(); + +admin.get('/', function (req: Request, res: Response) { + console.dir(admin.mountpath); // [ '/adm*n', '/manager' ] + res.send('Admin Homepage'); +}); + +const secret: Express = express(); +secret.get('/', function (req: Request, res: Response) { + console.log(secret.mountpath); // /secr*t + res.send('Admin Secret'); +}); + +admin.use('/secr*t', secret); // load the 'secret' router on '/secr*t', on the 'admin' sub app +app.use(['/adm*n', '/manager'], admin); // load the 'admin' router on '/adm*n' and '/manager', on the parent app +``` + ## Events ### app.on('mount') @@ -194,6 +240,23 @@ admin.get('/', function (req, res) { app.use('/admin', admin); ``` +```ts +import express, { type Express, type Application, type Request, type Response } from 'express'; + +const admin: Express = express(); + +admin.on('mount', function (parent: Application) { + console.log('Admin Mounted'); + console.log(parent); // refers to the parent app +}); + +admin.get('/', function (req: Request, res: Response) { + res.send('Admin Homepage'); +}); + +app.use('/admin', admin); +``` + ## Methods ### app.all() @@ -234,6 +297,15 @@ app.all('/secret', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.all('/secret', function (req: Request, res: Response, next: NextFunction) { + console.log('Accessing the secret section ...'); + next(); // pass control to the next handler +}); +``` + The `app.all()` method is useful for mapping "global" logic for specific path prefixes or arbitrary matches. For example, if you put the following at the top of all other route definitions, it requires that all routes from that point on require authentication, and automatically load a user. Keep in mind @@ -294,6 +366,14 @@ app.delete('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.delete('/', function (req: Request, res: Response) { + res.send('DELETE request to homepage'); +}); +``` + ### app.del() @@ -518,6 +598,14 @@ app.get('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', function (req: Request, res: Response) { + res.send('GET request to homepage'); +}); +``` + ### app.listen() @@ -564,6 +652,15 @@ app.listen(3000); // bind and listen on a TCP port app.listen('/tmp/sock'); // or listen on a UNIX socket ``` +```ts title="index.ts" +import express, { type Express } from 'express'; + +const app: Express = express(); + +app.listen(3000); // bind and listen on a TCP port +app.listen('/tmp/sock'); // or listen on a UNIX socket +``` + The `app` returned by `express()` is in fact a JavaScript `Function`, designed to be passed to Node's HTTP servers as a callback to handle requests. This makes it easy to provide both HTTP and HTTPS versions of @@ -591,6 +688,17 @@ http.createServer(app).listen(80); https.createServer(options, app).listen(443); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; +import https from 'https'; +import http from 'http'; + +const app: Express = express(); + +http.createServer(app).listen(80); +https.createServer(options, app).listen(443); +``` + The `app.listen()` method returns an [http.Server](https://nodejs.org/api/http.html#http_class_http_server) object and (for HTTP) is a convenience method for the following: ```js @@ -687,6 +795,23 @@ app['m-search']('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', function (req: Request, res: Response) { + res.send('GET request to homepage'); +}); + +app.post('/', function (req: Request, res: Response) { + res.send('POST request to homepage'); +}); + +// methods that are invalid JavaScript variable names use bracket notation +app['m-search']('/', function (req: Request, res: Response) { + res.send('M-SEARCH request to homepage'); +}); +``` + The `app.get()` function is automatically called for the HTTP `HEAD` method in addition to the @@ -759,6 +884,18 @@ app.query('/search', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Request, type Response } from 'express'; + +app.use(express.json()); // populate req.body for application/json payloads + +app.query('/search', (req: Request, res: Response) => { + // complex filter criteria arrive in req.body, not the URL + const results = db.search(req.body); + res.json(results); +}); +``` + ### app.param() @@ -817,6 +954,25 @@ app.get('/user/:id', function (req, res) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.param('id', function (req: Request, res: Response, next: NextFunction, id: string) { + console.log('CALLED ONLY ONCE'); + next(); +}); + +app.get('/user/:id', function (req: Request, res: Response, next: NextFunction) { + console.log('although this matches'); + next(); +}); + +app.get('/user/:id', function (req: Request, res: Response) { + console.log('and this matches too'); + res.end(); +}); +``` + On `GET /user/42`, the following is printed: ``` @@ -842,6 +998,28 @@ app.get('/user/:id/:page', function (req, res) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.param( + ['id', 'page'], + function (req: Request, res: Response, next: NextFunction, value: string) { + console.log('CALLED ONLY ONCE with', value); + next(); + } +); + +app.get('/user/:id/:page', function (req: Request, res: Response, next: NextFunction) { + console.log('although this matches'); + next(); +}); + +app.get('/user/:id/:page', function (req: Request, res: Response) { + console.log('and this matches too'); + res.end(); +}); +``` + On `GET /user/42/3`, the following is printed: ``` @@ -962,6 +1140,25 @@ router.get('[[\s\S]]*', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +// captures '1-a_6' but not '543-azser-sder' +router.get('/[0-9]+-[[\w]]*', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +// captures '1-a_6' and '543-az(ser"-sder' but not '5-a s' +router.get('/[0-9]+-[[\S]]*', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +// captures all (equivalent to '.*') +router.get('[[\s\S]]*', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + ### app.path() @@ -983,6 +1180,21 @@ console.dir(blog.path()); // '/blog' console.dir(blogAdmin.path()); // '/blog/admin' ``` +```ts title="index.ts" +import express, { type Express } from 'express'; + +const app: Express = express(); +const blog: Express = express(); +const blogAdmin: Express = express(); + +app.use('/blog', blog); +blog.use('/admin', blogAdmin); + +console.dir(app.path()); // '' +console.dir(blog.path()); // '/blog' +console.dir(blogAdmin.path()); // '/blog/admin' +``` + The behavior of this method can become very complicated in complex cases of mounted apps: it is usually better to use [req.baseUrl](/api/request/#reqbaseurl) to get the canonical path of the app. @@ -1020,6 +1232,14 @@ app.post('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.post('/', function (req: Request, res: Response) { + res.send('POST request to homepage'); +}); +``` + ### app.put() @@ -1053,6 +1273,14 @@ app.put('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.put('/', function (req: Request, res: Response) { + res.send('PUT request to homepage'); +}); +``` + ### app.render() @@ -1143,6 +1371,25 @@ app }); ``` +```ts +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +app + .route('/events') + .all(function (req: Request, res: Response, next: NextFunction) { + // runs for all HTTP verbs first + // think of it as route specific middleware! + }) + .get(function (req: Request, res: Response, next: NextFunction) { + res.json({}); + }) + .post(function (req: Request, res: Response, next: NextFunction) { + // maybe add a new event... + }); +``` + ### app.set() @@ -1418,6 +1665,15 @@ app.use(function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(function (req: Request, res: Response, next: NextFunction) { + console.log('Time: %d', Date.now()); + next(); +}); +``` + Sub-apps will: @@ -1443,6 +1699,20 @@ app.get('/', function (req, res) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +// this middleware will not allow the request to go beyond it +app.use(function (req: Request, res: Response, next: NextFunction) { + res.send('Hello World'); +}); + +// requests will never reach this route +app.get('/', function (req: Request, res: Response) { + res.send('Welcome'); +}); +``` + **Error-handling middleware** Error-handling middleware always takes _four_ arguments. You must provide four arguments to identify it as an error-handling middleware function. Even if you don't need to use the `next` object, you must specify it to maintain the signature. Otherwise, the `next` object will be interpreted as regular middleware and will fail to handle errors. For details about error-handling middleware, see: [Error handling](/guide/error-handling). @@ -1456,6 +1726,15 @@ app.use(function (err, req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(function (err: Error, req: Request, res: Response, next: NextFunction) { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + #### Path examples The following table provides some simple examples of valid `path` values for @@ -1469,6 +1748,14 @@ app.use('/abcd', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/abcd', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + **Path Pattern**: This will match paths starting with `/abcd` and `/abd`: ```js @@ -1477,6 +1764,14 @@ app.use('/abc?d', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/abc?d', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + This will match paths starting with `/abcd`, `/abbcd`, `/abbbbbcd`, and so on: ```js @@ -1485,6 +1780,14 @@ app.use('/ab+cd', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/ab+cd', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + This will match paths starting with `/abcd`, `/abxcd`, `/abFOOcd`, `/abbArcd`, and so on: ```js @@ -1493,6 +1796,14 @@ app.use('/ab*cd', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/ab*cd', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + This will match paths starting with `/ad` and `/abcd`: ```js @@ -1501,6 +1812,14 @@ app.use('/a(bc)?d', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/a(bc)?d', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + **Regular Expression**: This will match paths starting with `/abc` and `/xyz`: ```js @@ -1509,6 +1828,14 @@ app.use(/\/abc|\/xyz/, function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(/\/abc|\/xyz/, function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + **Array**: This will match paths starting with `/abcd`, `/xyza`, `/lmn`, and `/pqr`: ```js @@ -1517,6 +1844,17 @@ app.use(['/abcd', '/xyza', /\/lmn|\/pqr/], function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use( + ['/abcd', '/xyza', /\/lmn|\/pqr/], + function (req: Request, res: Response, next: NextFunction) { + next(); + } +); +``` + #### Middleware callback function examples The following table provides some simple examples of middleware functions that @@ -1530,6 +1868,14 @@ app.use(function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(function (req: Request, res: Response, next: NextFunction) { + next(); +}); +``` + A router is valid middleware. ```js @@ -1540,6 +1886,16 @@ router.get('/', function (req, res, next) { app.use(router); ``` +```ts +import express, { type Request, type Response, type NextFunction } from 'express'; + +const router = express.Router(); +router.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +app.use(router); +``` + An Express app is valid middleware. ```js @@ -1550,6 +1906,16 @@ subApp.get('/', function (req, res, next) { app.use(subApp); ``` +```ts +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const subApp: Express = express(); +subApp.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); +app.use(subApp); +``` + **Series of Middleware**: You can specify more than one middleware function at the same mount path. ```js @@ -1566,6 +1932,22 @@ r2.get('/', function (req, res, next) { app.use(r1, r2); ``` +```ts +import express, { type Request, type Response, type NextFunction } from 'express'; + +const r1 = express.Router(); +r1.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +const r2 = express.Router(); +r2.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +app.use(r1, r2); +``` + **Array**: Use an array to group middleware logically. ```js @@ -1582,6 +1964,22 @@ r2.get('/', function (req, res, next) { app.use([r1, r2]); ``` +```ts +import express, { type Request, type Response, type NextFunction } from 'express'; + +const r1 = express.Router(); +r1.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +const r2 = express.Router(); +r2.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +app.use([r1, r2]); +``` + **Combination**: You can combine all the above ways of mounting middleware. ```js @@ -1610,6 +2008,34 @@ subApp.get('/', function (req, res, next) { app.use(mw1, [mw2, r1, r2], subApp); ``` +```ts +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +function mw1(req: Request, res: Response, next: NextFunction) { + next(); +} +function mw2(req: Request, res: Response, next: NextFunction) { + next(); +} + +const r1 = express.Router(); +r1.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +const r2 = express.Router(); +r2.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +const subApp: Express = express(); +subApp.get('/', function (req: Request, res: Response, next: NextFunction) { + next(); +}); + +app.use(mw1, [mw2, r1, r2], subApp); +``` + Following are some examples of using the [express.static](/guide/using-middleware/#built-in-middleware) middleware in an Express app. diff --git a/src/content/api/4x/api/express/index.mdx b/src/content/api/4x/api/express/index.mdx index 8ee01d7c36..aa033d171e 100644 --- a/src/content/api/4x/api/express/index.mdx +++ b/src/content/api/4x/api/express/index.mdx @@ -19,6 +19,11 @@ import express from 'express'; const app = express(); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; +const app: Express = express(); +``` + ## Methods ### express.json() @@ -116,6 +121,20 @@ app.post('/profile', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of application/json +app.use(express.json()); + +app.post('/profile', (req: Request, res: Response) => { + console.dir(req.body); + res.json(req.body); +}); +``` + ### express.raw() @@ -202,6 +221,20 @@ app.post('/upload', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of application/octet-stream into a Buffer +app.use(express.raw()); + +app.post('/upload', (req: Request, res: Response) => { + console.dir(Buffer.isBuffer(req.body)); // => true + res.send(`received ${req.body.length} bytes`); +}); +``` + ### express.Router() @@ -401,6 +434,26 @@ const options = { app.use(express.static('public', options)); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; + +const app: Express = express(); + +const options = { + dotfiles: 'ignore', + etag: false, + extensions: ['htm', 'html'], + index: false, + maxAge: '1d', + redirect: false, + setHeaders: (res, path, stat) => { + res.set('x-timestamp', Date.now()); + }, +}; + +app.use(express.static('public', options)); +``` + ### express.text() @@ -491,6 +544,20 @@ app.post('/', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of text/plain into a string +app.use(express.text()); + +app.post('/', (req: Request, res: Response) => { + console.dir(typeof req.body); // => 'string' + res.send(req.body); +}); +``` + ### express.urlencoded() @@ -594,3 +661,17 @@ app.post('/profile', (req, res) => { res.json(req.body); }); ``` + +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of application/x-www-form-urlencoded +app.use(express.urlencoded({ extended: true })); + +app.post('/profile', (req: Request, res: Response) => { + console.dir(req.body); + res.json(req.body); +}); +``` diff --git a/src/content/api/4x/api/request/index.mdx b/src/content/api/4x/api/request/index.mdx index ce16198130..3a05b00d06 100644 --- a/src/content/api/4x/api/request/index.mdx +++ b/src/content/api/4x/api/request/index.mdx @@ -21,6 +21,14 @@ app.get('/user/:id', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', function (req: Request, res: Response) { + res.send('user ' + req.params.id); +}); +``` + But you could just as well have: ```js @@ -29,6 +37,14 @@ app.get('/user/:id', function (request, response) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', function (request: Request, response: Response) { + response.send('user ' + request.params.id); +}); +``` + The `req` object is an enhanced version of Node's own request object and supports all [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_incomingmessage). @@ -80,6 +96,14 @@ export default function (req, res) { } ``` +```ts title="mymiddleware.ts" +import { type Request, type Response } from 'express'; + +export default function (req: Request, res: Response) { + res.send('The views directory is ' + req.app.get('views')); +} +``` + ### req.baseUrl @@ -102,6 +126,19 @@ greet.get('/jp', function (req, res) { app.use('/greet', greet); // load the router on '/greet' ``` +```ts +import { type Request, type Response } from 'express'; + +const greet = express.Router(); + +greet.get('/jp', function (req: Request, res: Response) { + console.log(req.baseUrl); // /greet + res.send('Konnichiwa!'); +}); + +app.use('/greet', greet); // load the router on '/greet' +``` + Even if you use a path pattern or a set of path patterns to load the router, the `baseUrl` property returns the matched string, not the pattern(s). In the following example, the `greet` router is loaded on two path patterns. @@ -160,6 +197,20 @@ app.post('/profile', function (req, res, next) { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +app.use(express.json()); // for parsing application/json +app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded + +app.post('/profile', function (req: Request, res: Response, next: NextFunction) { + console.log(req.body); + res.json(req.body); +}); +``` + ### req.cookies @@ -290,6 +341,16 @@ app.use(function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(function (req: Request, res: Response, next: NextFunction) { + console.dir(req.method); + // => 'GET' + next(); +}); +``` + ### req.originalUrl @@ -324,6 +385,18 @@ app.use('/admin', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/admin', function (req: Request, res: Response, next: NextFunction) { + // GET 'http://www.example.com/admin/new?sort=desc' + console.dir(req.originalUrl); // '/admin/new?sort=desc' + console.dir(req.baseUrl); // '/admin' + console.dir(req.path); // '/new' + next(); +}); +``` + ### req.params @@ -427,6 +500,16 @@ app.set('query parser', function (str) { }); ``` +```ts title="index.ts" +import qs from 'qs'; + +app.set('query parser', function (str: string) { + return qs.parse(str, { + /* custom options */ + }); +}); +``` + Check out the [query parser application setting](/api/application/#application-settings) documentation for other customization options. ### req.res @@ -444,6 +527,16 @@ app.get('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', function (req: Request, res: Response) { + console.dir(req.res === res); + // => true + res.send('OK'); +}); +``` + ### req.route @@ -457,6 +550,15 @@ app.get('/user/:id?', function userIdHandler(req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id?', function userIdHandler(req: Request, res: Response) { + console.log(req.route); + res.send('GET'); +}); +``` + Example output from the previous snippet: ``` diff --git a/src/content/api/4x/api/response/index.mdx b/src/content/api/4x/api/response/index.mdx index d37499d684..93ed4e68d6 100644 --- a/src/content/api/4x/api/response/index.mdx +++ b/src/content/api/4x/api/response/index.mdx @@ -22,6 +22,14 @@ app.get('/user/:id', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', function (req: Request, res: Response) { + res.send('user ' + req.params.id); +}); +``` + But you could just as well have: ```js @@ -30,6 +38,14 @@ app.get('/user/:id', function (request, response) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', function (request: Request, response: Response) { + response.send('user ' + request.params.id); +}); +``` + The `res` object is an enhanced version of Node's own response object and supports all [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_serverresponse). @@ -52,6 +68,17 @@ app.get('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', function (req: Request, res: Response) { + console.dir(res.app.get('view engine')); + console.dir(res.app === req.app); + // => true + res.send('OK'); +}); +``` + ### res.headersSent @@ -66,6 +93,16 @@ app.get('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', function (req: Request, res: Response) { + console.dir(res.headersSent); // false + res.send('OK'); + console.dir(res.headersSent); // true +}); +``` + ### res.locals @@ -98,6 +135,17 @@ app.use(function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(function (req: Request, res: Response, next: NextFunction) { + // Make `user` and `authenticated` available in templates + res.locals.user = req.user; + res.locals.authenticated = !req.user.anonymous; + next(); +}); +``` + ### res.req @@ -113,6 +161,16 @@ app.get('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', function (req: Request, res: Response) { + console.dir(res.req === req); + // => true + res.send('OK'); +}); +``` + ## Methods ### res.append() @@ -978,6 +1036,30 @@ app.get('/file/:name', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/file/:name', function (req: Request, res: Response, next: NextFunction) { + const options = { + root: path.join(__dirname, 'public'), + dotfiles: 'deny', + headers: { + 'x-timestamp': Date.now(), + 'x-sent': true, + }, + }; + + const fileName = req.params.name; + res.sendFile(fileName, options, function (err) { + if (err) { + next(err); + } else { + console.log('Sent:', fileName); + } + }); +}); +``` + The following example illustrates using `res.sendFile` to provide fine-grained support for serving files: @@ -996,6 +1078,23 @@ app.get('/user/:uid/photos/:file', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:uid/photos/:file', function (req: Request, res: Response) { + const uid = req.params.uid; + const file = req.params.file; + + req.user.mayViewFilesFrom(uid, function (yes) { + if (yes) { + res.sendFile('/uploads/' + uid + '/' + file); + } else { + res.status(403).send("Sorry! You can't see that."); + } + }); +}); +``` + For more information, or if you have issues or concerns, see [send](https://github.com/pillarjs/send). ### res.sendfile() diff --git a/src/content/api/4x/api/router/index.mdx b/src/content/api/4x/api/router/index.mdx index 3c0fed0dc6..0311139d03 100644 --- a/src/content/api/4x/api/router/index.mdx +++ b/src/content/api/4x/api/router/index.mdx @@ -33,6 +33,22 @@ router.get('/events', function (req, res, next) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +// invoked for any requests passed to this router +router.use(function (req: Request, res: Response, next: NextFunction) { + // .. some logic here .. like any other middleware + next(); +}); + +// will handle any request that ends in /events +// depends on where the router is "use()'d" +router.get('/events', function (req: Request, res: Response, next: NextFunction) { + // .. +}); +``` + You can then use a router for a particular root URL in this way separating your routes into files or even mini-apps. ```js @@ -92,6 +108,19 @@ router.get('/events', (req, res) => { app.use('/calendar', router); ``` +```ts title="index.ts" +import express, { type Request, type Response } from 'express'; + +const router = express.Router({ caseSensitive: true, strict: true }); + +router.get('/events', (req: Request, res: Response) => { + res.send('events'); +}); + +// mount the router on an app +app.use('/calendar', router); +``` + ### router.all() @@ -186,6 +215,14 @@ router.get('/', function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +router.get('/', function (req: Request, res: Response) { + res.send('hello world'); +}); +``` + You can also use regular expressions—useful if you have very specific constraints, for example the following would match "GET /commits/71dbb9c" as well as "GET /commits/71dbb9c..4c084f9". @@ -198,6 +235,16 @@ router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function (req: Request, res: Response) { + const from = req.params[0]; + const to = req.params[1] || 'HEAD'; + res.send('commit range ' + from + '..' + to); +}); +``` + ### router.param() @@ -246,6 +293,24 @@ router.param('user', function (req, res, next, id) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +router.param('user', function (req: Request, res: Response, next: NextFunction, id) { + // try to get the user details from the User model and attach it to the request object + User.find(id, function (err, user) { + if (err) { + next(err); + } else if (user) { + req.user = user; + next(); + } else { + next(new Error('failed to load user')); + } + }); +}); +``` + Param callback functions are local to the router on which they are defined. They are not inherited by mounted apps or routers, nor are they triggered for route parameters inherited from parent routers. Hence, param callbacks defined on `router` will be triggered only by route parameters defined on `router` routes. A param callback will be called only once in a request-response cycle, even if the parameter is matched in multiple routes, as shown in the following examples. @@ -267,6 +332,25 @@ router.get('/user/:id', function (req, res) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +router.param('id', function (req: Request, res: Response, next: NextFunction, id) { + console.log('CALLED ONLY ONCE'); + next(); +}); + +router.get('/user/:id', function (req: Request, res: Response, next: NextFunction) { + console.log('although this matches'); + next(); +}); + +router.get('/user/:id', function (req: Request, res: Response) { + console.log('and this matches too'); + res.end(); +}); +``` + On `GET /user/42`, the following is printed: ``` @@ -352,6 +436,38 @@ app.listen(3000, function () { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); +const router = express.Router(); + +// customizing the behavior of router.param() +router.param(function (param, option) { + return function (req: Request, res: Response, next: NextFunction, val) { + if (val === option) { + next(); + } else { + res.sendStatus(403); + } + }; +}); + +// using the customized router.param() +router.param('id', '1337'); + +// route to trigger the capture +router.get('/user/:id', function (req: Request, res: Response) { + res.send('OK'); +}); + +app.use(router); + +app.listen(3000, function () { + console.log('Ready'); +}); +``` + In this example, the `router.param(name, callback)` signature remains the same, but instead of a middleware callback, a custom data type checking function has been defined to validate the data type of the user id. ```js @@ -370,6 +486,24 @@ router.param('id', function (candidate) { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +router.param(function (param, validator) { + return function (req: Request, res: Response, next: NextFunction, val) { + if (validator(val)) { + next(); + } else { + res.sendStatus(403); + } + }; +}); + +router.param('id', function (candidate) { + return !isNaN(parseFloat(candidate)) && isFinite(candidate); +}); +``` + ### router.route() @@ -423,6 +557,44 @@ router }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const router = express.Router(); + +router.param('user_id', function (req: Request, res: Response, next: NextFunction, id) { + // sample user, would actually fetch from DB, etc... + req.user = { + id: id, + name: 'TJ', + }; + next(); +}); + +router + .route('/users/:user_id') + .all(function (req: Request, res: Response, next: NextFunction) { + // runs for all HTTP verbs first + // think of it as route specific middleware! + next(); + }) + .get(function (req: Request, res: Response, next: NextFunction) { + res.json(req.user); + }) + .put(function (req: Request, res: Response, next: NextFunction) { + // just an example of maybe updating the user + req.user.name = req.params.name; + // save user ... etc + res.json(req.user); + }) + .post(function (req: Request, res: Response, next: NextFunction) { + next(new Error('not implemented')); + }) + .delete(function (req: Request, res: Response, next: NextFunction) { + next(new Error('not implemented')); + }); +``` + This approach re-uses the single `/users/:user_id` path and adds handlers for various HTTP methods. @@ -512,6 +684,35 @@ app.use('/foo', router); app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); +const router = express.Router(); + +// simple logger for this router's requests +// all requests to this router will first hit this middleware +router.use(function (req: Request, res: Response, next: NextFunction) { + console.log('%s %s %s', req.method, req.url, req.path); + next(); +}); + +// this will only be invoked if the path starts with /bar from the mount point +router.use('/bar', function (req: Request, res: Response, next: NextFunction) { + // ... maybe some additional /bar logging ... + next(); +}); + +// always invoked +router.use(function (req: Request, res: Response, next: NextFunction) { + res.send('Hello World'); +}); + +app.use('/foo', router); + +app.listen(3000); +``` + The "mount" path is stripped and is _not_ visible to the middleware function. The main effect of this feature is that a mounted middleware function may operate without code changes regardless of its "prefix" pathname. @@ -542,6 +743,18 @@ router.use(function (req, res) { }); ``` +```ts title="index.ts" +import logger from 'morgan'; +import path from 'path'; +import { type Request, type Response } from 'express'; + +router.use(logger()); +router.use(express.static(path.join(__dirname, 'public'))); +router.use(function (req: Request, res: Response) { + res.send('Hello'); +}); +``` + Now suppose you wanted to ignore logging requests for static files, but to continue logging routes and middleware defined after `logger()`. You would simply move the call to `express.static()` to the top, before adding the logger middleware: @@ -554,6 +767,16 @@ router.use(function (req, res) { }); ``` +```ts +import { type Request, type Response } from 'express'; + +router.use(express.static(path.join(__dirname, 'public'))); +router.use(logger()); +router.use(function (req: Request, res: Response) { + res.send('Hello'); +}); +``` + Another example is serving files from multiple directories, giving precedence to "./public" over the others: @@ -597,4 +820,27 @@ app.use('/users', authRouter); app.use('/users', openRouter); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; +import { basic } from './authenticate'; + +const authRouter = express.Router(); +const openRouter = express.Router(); + +authRouter.use(basic(usersdb)); + +authRouter.get('/:user_id/edit', function (req: Request, res: Response, next: NextFunction) { + // ... Edit user UI ... +}); +openRouter.get('/', function (req: Request, res: Response, next: NextFunction) { + // ... List users ... +}); +openRouter.get('/:user_id', function (req: Request, res: Response, next: NextFunction) { + // ... View user ... +}); + +app.use('/users', authRouter); +app.use('/users', openRouter); +``` + Even though the authentication middleware was added via the `authRouter` it will run on the routes defined by the `openRouter` as well since both routers were mounted on `/users`. To avoid this behavior, use different paths for each router. diff --git a/src/content/api/5x/api.mdx b/src/content/api/5x/api.mdx index 1e9af07c3a..e99c9040b3 100644 --- a/src/content/api/5x/api.mdx +++ b/src/content/api/5x/api.mdx @@ -33,6 +33,18 @@ app.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', (req: Request, res: Response) => { + res.send('hello world'); +}); + +app.listen(3000); +``` + Express 5.x requires **Node.js 18 or higher**. diff --git a/src/content/api/5x/api/application/index.mdx b/src/content/api/5x/api/application/index.mdx index 27629e173e..88ceb19322 100644 --- a/src/content/api/5x/api/application/index.mdx +++ b/src/content/api/5x/api/application/index.mdx @@ -33,6 +33,18 @@ app.get('/', function (req, res) { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', function (req: Request, res: Response) { + res.send('hello world'); +}); + +app.listen(3000); +``` + The `app` object has methods for - Routing HTTP requests; see for example, [app.METHOD](#appmethod) and [app.param](#appparam). @@ -129,6 +141,20 @@ admin.get('/', (req, res) => { app.use('/admin', admin); // mount the sub app ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); // the main app +const admin: Express = express(); // the sub app + +admin.get('/', (req: Request, res: Response) => { + console.log(admin.mountpath); // /admin + res.send('Admin Homepage'); +}); + +app.use('/admin', admin); // mount the sub app +``` + It is similar to the [baseUrl](/api/request/#reqbaseurl) property of the `req` object, except `req.baseUrl` returns the matched URL path, instead of the matched patterns. @@ -153,6 +179,26 @@ admin.use('/secr{*splat}t', secret); // load the 'secret' router on '/secr{*spla app.use(['/adm{*splat}n', '/manager'], admin); // load the 'admin' router on '/adm{*splat}n' and '/manager', on the parent app ``` +```ts +import express, { type Express, type Request, type Response } from 'express'; + +const admin: Express = express(); + +admin.get('/', (req: Request, res: Response) => { + console.log(admin.mountpath); // [ '/adm{*splat}n', '/manager' ] + res.send('Admin Homepage'); +}); + +const secret: Express = express(); +secret.get('/', (req: Request, res: Response) => { + console.log(secret.mountpath); // /secr{*splat}t + res.send('Admin Secret'); +}); + +admin.use('/secr{*splat}t', secret); // load the 'secret' router on '/secr{*splat}t', on the 'admin' sub app +app.use(['/adm{*splat}n', '/manager'], admin); // load the 'admin' router on '/adm{*splat}n' and '/manager', on the parent app +``` + ### app.router @@ -184,6 +230,19 @@ router.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); +const router = app.router; + +router.get('/', (req: Request, res: Response) => { + res.send('hello world'); +}); + +app.listen(3000); +``` + You can add middleware and HTTP method routes to the `router` just like an application. For more information, see [Router](/api/router/). @@ -229,6 +288,23 @@ admin.get('/', (req, res) => { app.use('/admin', admin); ``` +```ts +import express, { type Express, type Application, type Request, type Response } from 'express'; + +const admin: Express = express(); + +admin.on('mount', (parent: Application) => { + console.log('Admin Mounted'); + console.log(parent); // refers to the parent app +}); + +admin.get('/', (req: Request, res: Response) => { + res.send('Admin Homepage'); +}); + +app.use('/admin', admin); +``` + ## Methods ### app.all() @@ -269,6 +345,15 @@ app.all('/secret', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.all('/secret', (req: Request, res: Response, next: NextFunction) => { + console.log('Accessing the secret section ...'); + next(); // pass control to the next handler +}); +``` + The `app.all()` method is useful for mapping "global" logic for specific path prefixes or arbitrary matches. For example, if you put the following at the top of all other route definitions, it requires that all routes from that point on require authentication, and automatically load a user. Keep in mind @@ -329,6 +414,14 @@ app.delete('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.delete('/', (req: Request, res: Response) => { + res.send('DELETE request to homepage'); +}); +``` + ### app.disable() @@ -530,6 +623,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.send('GET request to homepage'); +}); +``` + ### app.listen() @@ -577,6 +678,15 @@ app.listen(3000); // bind and listen on a TCP port app.listen('/tmp/sock'); // or listen on a UNIX socket ``` +```ts title="index.ts" +import express, { type Express } from 'express'; + +const app: Express = express(); + +app.listen(3000); // bind and listen on a TCP port +app.listen('/tmp/sock'); // or listen on a UNIX socket +``` + When the server emits an `error` event (such as `EADDRINUSE`), the error is passed to the provided @@ -620,6 +730,17 @@ http.createServer(app).listen(80); https.createServer(options, app).listen(443); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; +import https from 'https'; +import http from 'http'; + +const app: Express = express(); + +http.createServer(app).listen(80); +https.createServer(options, app).listen(443); +``` + The `app.listen()` method returns an [http.Server](https://nodejs.org/api/http.html#http_class_http_server) object and (for HTTP) is a convenience method for the following: ```js @@ -718,6 +839,23 @@ app['m-search']('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.send('GET request to homepage'); +}); + +app.post('/', (req: Request, res: Response) => { + res.send('POST request to homepage'); +}); + +// methods that are invalid JavaScript variable names use bracket notation +app['m-search']('/', (req: Request, res: Response) => { + res.send('M-SEARCH request to homepage'); +}); +``` + The `app.get()` function is automatically called for the HTTP `HEAD` method in addition to the @@ -789,6 +927,25 @@ app.get('/user/:id', (req, res) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.param('id', (req: Request, res: Response, next: NextFunction, id: string) => { + console.log('CALLED ONLY ONCE'); + next(); +}); + +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + console.log('although this matches'); + next(); +}); + +app.get('/user/:id', (req: Request, res: Response) => { + console.log('and this matches too'); + res.end(); +}); +``` + On `GET /user/42`, the following is printed: ``` @@ -814,6 +971,25 @@ app.get('/user/:id/:page', (req, res) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.param(['id', 'page'], (req: Request, res: Response, next: NextFunction, value: string) => { + console.log('CALLED ONLY ONCE with', value); + next(); +}); + +app.get('/user/:id/:page', (req: Request, res: Response, next: NextFunction) => { + console.log('although this matches'); + next(); +}); + +app.get('/user/:id/:page', (req: Request, res: Response) => { + console.log('and this matches too'); + res.end(); +}); +``` + On `GET /user/42/3`, the following is printed: ``` @@ -842,6 +1018,21 @@ console.log(blog.path()); // '/blog' console.log(blogAdmin.path()); // '/blog/admin' ``` +```ts title="index.ts" +import express, { type Express } from 'express'; + +const app: Express = express(); +const blog: Express = express(); +const blogAdmin: Express = express(); + +app.use('/blog', blog); +blog.use('/admin', blogAdmin); + +console.log(app.path()); // '' +console.log(blog.path()); // '/blog' +console.log(blogAdmin.path()); // '/blog/admin' +``` + The behavior of this method can become very complicated in complex cases of mounted apps: it is usually better to use [req.baseUrl](/api/request/#reqbaseurl) to get the canonical path of the app. @@ -879,6 +1070,14 @@ app.post('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.post('/', (req: Request, res: Response) => { + res.send('POST request to homepage'); +}); +``` + ### app.put() @@ -912,6 +1111,14 @@ app.put('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.put('/', (req: Request, res: Response) => { + res.send('PUT request to homepage'); +}); +``` + ### app.query() =20.19.3 <21 || >=22.2.0' }}> @@ -971,6 +1178,18 @@ app.query('/search', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Request, type Response } from 'express'; + +app.use(express.json()); // populate req.body for application/json payloads + +app.query('/search', (req: Request, res: Response) => { + // complex filter criteria arrive in req.body, not the URL + const results = db.search(req.body); + res.json(results); +}); +``` + ### app.render() @@ -1061,6 +1280,25 @@ app }); ``` +```ts +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +app + .route('/events') + .all((req: Request, res: Response, next: NextFunction) => { + // runs for all HTTP verbs first + // think of it as route specific middleware! + }) + .get((req: Request, res: Response, next: NextFunction) => { + res.json({}); + }) + .post((req: Request, res: Response, next: NextFunction) => { + // maybe add a new event... + }); +``` + ### app.set() @@ -1336,6 +1574,15 @@ app.use((req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + console.log('Time: %d', Date.now()); + next(); +}); +``` + Sub-apps will: @@ -1361,6 +1608,20 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +// this middleware will not allow the request to go beyond it +app.use((req: Request, res: Response, next: NextFunction) => { + res.send('Hello World'); +}); + +// requests will never reach this route +app.get('/', (req: Request, res: Response) => { + res.send('Welcome'); +}); +``` + **Error-handling middleware** Error-handling middleware always takes _four_ arguments. You must provide four arguments to identify it as an error-handling middleware function. Even if you don't need to use the `next` object, you must specify it to maintain the signature. Otherwise, the `next` object will be interpreted as regular middleware and will fail to handle errors. For details about error-handling middleware, see: [Error handling](/guide/error-handling). @@ -1374,6 +1635,15 @@ app.use((err, req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + #### Path examples The following table provides some simple examples of valid `path` values for @@ -1387,6 +1657,14 @@ app.use('/abcd', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/abcd', (req: Request, res: Response, next: NextFunction) => { + next(); +}); +``` + **Path Pattern**: This will match paths starting with `/abcd` and `/abd`: ```js @@ -1395,6 +1673,14 @@ app.use('/ab{c}d', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/ab{c}d', (req: Request, res: Response, next: NextFunction) => { + next(); +}); +``` + **Regular Expression**: This will match paths starting with `/abc` and `/xyz`: ```js @@ -1403,6 +1689,14 @@ app.use(/\/abc|\/xyz/, (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(/\/abc|\/xyz/, (req: Request, res: Response, next: NextFunction) => { + next(); +}); +``` + **Array**: This will match paths starting with `/abcd`, `/xyza`, `/lmn`, and `/pqr`: ```js @@ -1411,6 +1705,14 @@ app.use(['/abcd', '/xyza', /\/lmn|\/pqr/], (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use(['/abcd', '/xyza', /\/lmn|\/pqr/], (req: Request, res: Response, next: NextFunction) => { + next(); +}); +``` + #### Middleware callback function examples The following table provides some simple examples of middleware functions that @@ -1424,6 +1726,14 @@ app.use((req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + next(); +}); +``` + A router is valid middleware. ```js @@ -1434,6 +1744,16 @@ router.get('/', (req, res, next) => { app.use(router); ``` +```ts +import express, { type Request, type Response, type NextFunction } from 'express'; + +const router = express.Router(); +router.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); +app.use(router); +``` + An Express app is valid middleware. ```js @@ -1444,6 +1764,16 @@ subApp.get('/', (req, res, next) => { app.use(subApp); ``` +```ts +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const subApp: Express = express(); +subApp.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); +app.use(subApp); +``` + **Series of Middleware**: You can specify more than one middleware function at the same mount path. ```js @@ -1460,6 +1790,22 @@ r2.get('/', (req, res, next) => { app.use(r1, r2); ``` +```ts +import express, { type Request, type Response, type NextFunction } from 'express'; + +const r1 = express.Router(); +r1.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); + +const r2 = express.Router(); +r2.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); + +app.use(r1, r2); +``` + **Array**: Use an array to group middleware logically. ```js @@ -1476,6 +1822,22 @@ r2.get('/', (req, res, next) => { app.use([r1, r2]); ``` +```ts +import express, { type Request, type Response, type NextFunction } from 'express'; + +const r1 = express.Router(); +r1.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); + +const r2 = express.Router(); +r2.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); + +app.use([r1, r2]); +``` + **Combination**: You can combine all the above ways of mounting middleware. ```js @@ -1504,6 +1866,34 @@ subApp.get('/', (req, res, next) => { app.use(mw1, [mw2, r1, r2], subApp); ``` +```ts +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +function mw1(req: Request, res: Response, next: NextFunction) { + next(); +} +function mw2(req: Request, res: Response, next: NextFunction) { + next(); +} + +const r1 = express.Router(); +r1.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); + +const r2 = express.Router(); +r2.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); + +const subApp: Express = express(); +subApp.get('/', (req: Request, res: Response, next: NextFunction) => { + next(); +}); + +app.use(mw1, [mw2, r1, r2], subApp); +``` + Following are some examples of using the [express.static](/guide/using-middleware/#built-in-middleware) middleware in an Express app. diff --git a/src/content/api/5x/api/express/index.mdx b/src/content/api/5x/api/express/index.mdx index 36adbe9419..ea7ede8e74 100644 --- a/src/content/api/5x/api/express/index.mdx +++ b/src/content/api/5x/api/express/index.mdx @@ -20,6 +20,12 @@ import express from 'express'; const app = express(); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; + +const app: Express = express(); +``` + ## Methods The Express object has the following methods that can be used to create middleware functions, routers and have some built-in middleware: @@ -123,6 +129,20 @@ app.post('/profile', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of application/json +app.use(express.json()); + +app.post('/profile', (req: Request, res: Response) => { + console.dir(req.body); + res.json(req.body); +}); +``` + ### express.raw() @@ -209,6 +229,20 @@ app.post('/upload', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of application/octet-stream into a Buffer +app.use(express.raw()); + +app.post('/upload', (req: Request, res: Response) => { + console.dir(Buffer.isBuffer(req.body)); // => true + res.send(`received ${req.body.length} bytes`); +}); +``` + ### express.Router() @@ -407,6 +441,26 @@ const options = { app.use(express.static('public', options)); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; + +const app: Express = express(); + +const options = { + dotfiles: 'ignore', + etag: false, + extensions: ['htm', 'html'], + index: false, + maxAge: '1d', + redirect: false, + setHeaders(res, path, stat) { + res.set('x-timestamp', Date.now()); + }, +}; + +app.use(express.static('public', options)); +``` + ### express.text() @@ -497,6 +551,20 @@ app.post('/', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of text/plain into a string +app.use(express.text()); + +app.post('/', (req: Request, res: Response) => { + console.dir(typeof req.body); // => 'string' + res.send(req.body); +}); +``` + ### express.urlencoded() @@ -611,3 +679,17 @@ app.post('/profile', (req, res) => { res.json(req.body); }); ``` + +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// parse requests with a Content-Type of application/x-www-form-urlencoded +app.use(express.urlencoded({ extended: true })); + +app.post('/profile', (req: Request, res: Response) => { + console.dir(req.body); + res.json(req.body); +}); +``` diff --git a/src/content/api/5x/api/request/index.mdx b/src/content/api/5x/api/request/index.mdx index a5a007fb0d..9960e91997 100644 --- a/src/content/api/5x/api/request/index.mdx +++ b/src/content/api/5x/api/request/index.mdx @@ -20,6 +20,14 @@ app.get('/user/:id', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', (req: Request, res: Response) => { + res.send(`user ${req.params.id}`); +}); +``` + But you could just as well have: ```js @@ -28,6 +36,14 @@ app.get('/user/:id', (request, response) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', (request: Request, response: Response) => { + response.send(`user ${request.params.id}`); +}); +``` + The `req` object is an enhanced version of Node's own request object and supports all [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_incomingmessage). @@ -70,6 +86,14 @@ export default (req, res) => { }; ``` +```ts title="mymiddleware.ts" +import { type Request, type Response } from 'express'; + +export default (req: Request, res: Response) => { + res.send(`The views directory is ${req.app.get('views')}`); +}; +``` + ### req.baseUrl @@ -92,6 +116,19 @@ greet.get('/jp', (req, res) => { app.use('/greet', greet); // load the router on '/greet' ``` +```ts +import express, { type Request, type Response } from 'express'; + +const greet = express.Router(); + +greet.get('/jp', (req: Request, res: Response) => { + console.log(req.baseUrl); // /greet + res.send('Konnichiwa!'); +}); + +app.use('/greet', greet); // load the router on '/greet' +``` + Even if you use a path pattern or a set of path patterns to load the router, the `baseUrl` property returns the matched string, not the pattern(s). In the following example, the `greet` router is loaded on two path patterns. @@ -150,6 +187,20 @@ app.post('/profile', (req, res, next) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +app.use(express.json()); // for parsing application/json +app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded + +app.post('/profile', (req: Request, res: Response, next: NextFunction) => { + console.log(req.body); + res.json(req.body); +}); +``` + ### req.cookies @@ -282,6 +333,16 @@ app.use((req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + console.dir(req.method); + // => 'GET' + next(); +}); +``` + ### req.originalUrl @@ -316,6 +377,18 @@ app.use('/admin', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +// GET 'http://www.example.com/admin/new?sort=desc' +app.use('/admin', (req: Request, res: Response, next: NextFunction) => { + console.dir(req.originalUrl); // '/admin/new?sort=desc' + console.dir(req.baseUrl); // '/admin' + console.dir(req.path); // '/new' + next(); +}); +``` + ### req.params @@ -340,6 +413,18 @@ app.get('/files/*file', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/files/*file', (req: Request, res: Response) => { + console.dir(req.params.file); + // GET /files/note.txt + // => [ 'note.txt' ] + // GET /files/images/image.png + // => [ 'images', 'image.png' ] +}); +``` + When you use a regular expression for the route definition, capture groups are provided as integer keys using `req.params[n]`, where `n` is the nth capture group. ```js @@ -350,6 +435,16 @@ app.use(/^\/file\/(.*)$/, (req, res) => { }); ``` +```ts +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" +}); +``` + Named capturing groups in regular expressions behave like named route parameters. For example the group from `/^\/file\/(?.*)$/` expression is available as `req.params.path`. If you need to make changes to a key in `req.params`, use the [app.param](/api/application/#appparam) handler. Changes are applicable only to [parameters](/guide/routing/#route-parameters) already defined in the route path. @@ -433,6 +528,16 @@ app.set('query parser', (str) => ); ``` +```ts title="index.ts" +import qs from 'qs'; + +app.set('query parser', (str: string) => + qs.parse(str, { + /* custom options */ + }) +); +``` + Check out the [query parser application setting](/api/application/#application-settings) documentation for other customization options. ### req.res @@ -450,6 +555,16 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + console.dir(req.res === res); + // => true + res.send('OK'); +}); +``` + ### req.route @@ -463,6 +578,15 @@ app.get('/user/{:id}', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/{:id}', (req: Request, res: Response) => { + console.dir(req.route, { depth: null }); + res.send('GET'); +}); +``` + Example output from the previous snippet: ``` diff --git a/src/content/api/5x/api/response/index.mdx b/src/content/api/5x/api/response/index.mdx index 7f2e3b4e09..1217cf7b34 100644 --- a/src/content/api/5x/api/response/index.mdx +++ b/src/content/api/5x/api/response/index.mdx @@ -21,6 +21,14 @@ app.get('/user/:id', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', (req: Request, res: Response) => { + res.send(`user ${req.params.id}`); +}); +``` + But you could just as well have: ```js @@ -29,6 +37,14 @@ app.get('/user/:id', (request, response) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:id', (request: Request, response: Response) => { + response.send(`user ${request.params.id}`); +}); +``` + The `res` object is an enhanced version of Node's own response object and supports all [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_serverresponse). @@ -51,6 +67,17 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + console.dir(res.app.get('view engine')); + console.dir(res.app === req.app); + // => true + res.send('OK'); +}); +``` + ### res.headersSent @@ -65,6 +92,16 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + console.log(res.headersSent); // false + res.send('OK'); + console.log(res.headersSent); // true +}); +``` + ### res.locals @@ -97,6 +134,17 @@ app.use((req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + // Make `user` and `authenticated` available in templates + res.locals.user = req.user; + res.locals.authenticated = !req.user.anonymous; + next(); +}); +``` + ### res.req @@ -112,6 +160,16 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + console.dir(res.req === req); + // => true + res.send('OK'); +}); +``` + ## Methods ### res.append() @@ -894,6 +952,30 @@ app.get('/file/:name', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/file/:name', (req: Request, res: Response, next: NextFunction) => { + const options = { + root: path.join(__dirname, 'public'), + dotfiles: 'deny', + headers: { + 'x-timestamp': Date.now(), + 'x-sent': true, + }, + }; + + const fileName = req.params.name; + res.sendFile(fileName, options, (err) => { + if (err) { + next(err); + } else { + console.log('Sent:', fileName); + } + }); +}); +``` + The following example illustrates using `res.sendFile` to provide fine-grained support for serving files: @@ -912,6 +994,23 @@ app.get('/user/:uid/photos/:file', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/user/:uid/photos/:file', (req: Request, res: Response) => { + const uid = req.params.uid; + const file = req.params.file; + + req.user.mayViewFilesFrom(uid, (yes) => { + if (yes) { + res.sendFile(`/uploads/${uid}/${file}`); + } else { + res.status(403).send("Sorry! You can't see that."); + } + }); +}); +``` + For more information, or if you have issues or concerns, see [send](https://github.com/pillarjs/send). ### res.sendStatus() diff --git a/src/content/api/5x/api/router/index.mdx b/src/content/api/5x/api/router/index.mdx index 764c35101c..af72f1c1a1 100644 --- a/src/content/api/5x/api/router/index.mdx +++ b/src/content/api/5x/api/router/index.mdx @@ -33,6 +33,22 @@ router.get('/events', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +// invoked for any requests passed to this router +router.use((req: Request, res: Response, next: NextFunction) => { + // .. some logic here .. like any other middleware + next(); +}); + +// will handle any request that ends in /events +// depends on where the router is "use()'d" +router.get('/events', (req: Request, res: Response, next: NextFunction) => { + // .. +}); +``` + You can then use a router for a particular root URL in this way separating your routes into files or even mini-apps. ```js @@ -92,6 +108,19 @@ router.get('/events', (req, res) => { app.use('/calendar', router); ``` +```ts title="index.ts" +import express, { type Request, type Response } from 'express'; + +const router = express.Router({ caseSensitive: true, strict: true }); + +router.get('/events', (req: Request, res: Response) => { + res.send('events'); +}); + +// mount the router on an app +app.use('/calendar', router); +``` + ### router.all() @@ -186,6 +215,14 @@ router.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +router.get('/', (req: Request, res: Response) => { + res.send('hello world'); +}); +``` + You can also use regular expressions—useful if you have very specific constraints, for example the following would match "GET /commits/71dbb9c" as well as "GET /commits/71dbb9c..4c084f9". @@ -198,6 +235,16 @@ router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, (req: Request, res: Response) => { + const from = req.params[0]; + const to = req.params[1] || 'HEAD'; + res.send(`commit range ${from}..${to}`); +}); +``` + You can use `next` primitive to implement a flow control between different middleware functions, based on a specific program state. Invoking `next` with the string `'router'` will cause all the remaining route callbacks on that router @@ -222,6 +269,25 @@ app.get('/foo', (req, res) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function fn(req: Request, res: Response, next: NextFunction) { + console.log('I come here'); + next('router'); +} +router.get('/foo', fn, (req: Request, res: Response, next: NextFunction) => { + console.log("I don't come here"); +}); +router.get('/foo', (req: Request, res: Response, next: NextFunction) => { + console.log("I don't come here"); +}); +app.get('/foo', (req: Request, res: Response) => { + console.log(' I come here too'); + res.end('good'); +}); +``` + ### router.param() @@ -270,6 +336,24 @@ router.param('user', (req, res, next, id) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +router.param('user', (req: Request, res: Response, next: NextFunction, id: string) => { + // try to get the user details from the User model and attach it to the request object + User.find(id, (err, user) => { + if (err) { + next(err); + } else if (user) { + req.user = user; + next(); + } else { + next(new Error('failed to load user')); + } + }); +}); +``` + Param callback functions are local to the router on which they are defined. They are not inherited by mounted apps or routers, nor are they triggered for route parameters inherited from parent routers. Hence, param callbacks defined on `router` will be triggered only by route parameters defined on `router` routes. A param callback will be called only once in a request-response cycle, even if the parameter is matched in multiple routes, as shown in the following examples. @@ -291,6 +375,25 @@ router.get('/user/:id', (req, res) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +router.param('id', (req: Request, res: Response, next: NextFunction, id: string) => { + console.log('CALLED ONLY ONCE'); + next(); +}); + +router.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + console.log('although this matches'); + next(); +}); + +router.get('/user/:id', (req: Request, res: Response) => { + console.log('and this matches too'); + res.end(); +}); +``` + On `GET /user/42`, the following is printed: ``` @@ -352,6 +455,44 @@ router }); ``` +```ts title="index.ts" +import express, { type Request, type Response, type NextFunction } from 'express'; + +const router = express.Router(); + +router.param('user_id', (req: Request, res: Response, next: NextFunction, id: string) => { + // sample user, would actually fetch from DB, etc... + req.user = { + id, + name: 'TJ', + }; + next(); +}); + +router + .route('/users/:user_id') + .all((req: Request, res: Response, next: NextFunction) => { + // runs for all HTTP verbs first + // think of it as route specific middleware! + next(); + }) + .get((req: Request, res: Response, next: NextFunction) => { + res.json(req.user); + }) + .put((req: Request, res: Response, next: NextFunction) => { + // just an example of maybe updating the user + req.user.name = req.params.name; + // save user ... etc + res.json(req.user); + }) + .post((req: Request, res: Response, next: NextFunction) => { + next(new Error('not implemented')); + }) + .delete((req: Request, res: Response, next: NextFunction) => { + next(new Error('not implemented')); + }); +``` + This approach re-uses the single `/users/:user_id` path and adds handlers for various HTTP methods. @@ -441,6 +582,35 @@ app.use('/foo', router); app.listen(3000); ``` +```ts title="index.ts" +import express, { type Request, type Response, type NextFunction } from 'express'; + +const app = express(); +const router = express.Router(); + +// simple logger for this router's requests +// all requests to this router will first hit this middleware +router.use((req: Request, res: Response, next: NextFunction) => { + console.log('%s %s %s', req.method, req.url, req.path); + next(); +}); + +// this will only be invoked if the path starts with /bar from the mount point +router.use('/bar', (req: Request, res: Response, next: NextFunction) => { + // ... maybe some additional /bar logging ... + next(); +}); + +// always invoked +router.use((req: Request, res: Response, next: NextFunction) => { + res.send('Hello World'); +}); + +app.use('/foo', router); + +app.listen(3000); +``` + The "mount" path is stripped and is _not_ visible to the middleware function. The main effect of this feature is that a mounted middleware function may operate without code changes regardless of its "prefix" pathname. @@ -469,6 +639,17 @@ router.use((req, res) => { }); ``` +```ts title="index.ts" +import logger from 'morgan'; +import { type Request, type Response } from 'express'; + +router.use(logger()); +router.use(express.static(path.join(__dirname, 'public'))); +router.use((req: Request, res: Response) => { + res.send('Hello'); +}); +``` + Now suppose you wanted to ignore logging requests for static files, but to continue logging routes and middleware defined after `logger()`. You would simply move the call to `express.static()` to the top, before adding the logger middleware: @@ -481,6 +662,16 @@ router.use((req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +router.use(express.static(path.join(__dirname, 'public'))); +router.use(logger()); +router.use((req: Request, res: Response) => { + res.send('Hello'); +}); +``` + Another example is serving files from multiple directories, giving precedence to "./public" over the others: @@ -524,4 +715,27 @@ app.use('/users', authRouter); app.use('/users', openRouter); ``` +```ts +import express, { type Request, type Response, type NextFunction } from 'express'; +import { basic } from './authenticate'; + +const authRouter = express.Router(); +const openRouter = express.Router(); + +authRouter.use(basic(usersdb)); + +authRouter.get('/:user_id/edit', (req: Request, res: Response, next: NextFunction) => { + // ... Edit user UI ... +}); +openRouter.get('/', (req: Request, res: Response, next: NextFunction) => { + // ... List users ... +}); +openRouter.get('/:user_id', (req: Request, res: Response, next: NextFunction) => { + // ... View user ... +}); + +app.use('/users', authRouter); +app.use('/users', openRouter); +``` + Even though the authentication middleware was added via the `authRouter` it will run on the routes defined by the `openRouter` as well since both routers were mounted on `/users`. To avoid this behavior, use different paths for each router. diff --git a/src/content/docs/en/4x/advanced/developing-template-engines.md b/src/content/docs/en/4x/advanced/developing-template-engines.mdx similarity index 69% rename from src/content/docs/en/4x/advanced/developing-template-engines.md rename to src/content/docs/en/4x/advanced/developing-template-engines.mdx index 6e9710fbc5..bf5ae9b2f1 100755 --- a/src/content/docs/en/4x/advanced/developing-template-engines.md +++ b/src/content/docs/en/4x/advanced/developing-template-engines.mdx @@ -25,6 +25,31 @@ app.set('views', './views'); // specify the views directory app.set('view engine', 'ntl'); // register the template engine ``` +```ts +import fs from 'fs'; // this engine requires the fs module +app.engine( + 'ntl', + ( + filePath: string, + options: Record, + callback: (e: any, rendered?: string) => void + ) => { + // define the template engine + fs.readFile(filePath, (err, content) => { + if (err) return callback(err); + // this is an extremely simple template engine + const rendered = content + .toString() + .replace('#title#', `${options.title}`) + .replace('#message#', `

${options.message}

`); + return callback(null, rendered); + }); + } +); +app.set('views', './views'); // specify the views directory +app.set('view engine', 'ntl'); // register the template engine +``` + Your app will now be able to render `.ntl` files. Create a file named `index.ntl` in the `views` directory with the following content. ```pug diff --git a/src/content/docs/en/4x/guide/behind-proxies.mdx b/src/content/docs/en/4x/guide/behind-proxies.mdx index bc2e774077..dd1f1d00df 100644 --- a/src/content/docs/en/4x/guide/behind-proxies.mdx +++ b/src/content/docs/en/4x/guide/behind-proxies.mdx @@ -77,6 +77,14 @@ app.set('trust proxy', (ip) => { }); ``` +```ts +app.set('trust proxy', (ip: string) => { + if (ip === '127.0.0.1' || ip === '123.123.123.123') + return true; // trusted IPs + else return false; +}); +``` + diff --git a/src/content/docs/en/4x/guide/error-handling.mdx b/src/content/docs/en/4x/guide/error-handling.mdx index 3d65d4d3b6..7cafabc5f6 100644 --- a/src/content/docs/en/4x/guide/error-handling.mdx +++ b/src/content/docs/en/4x/guide/error-handling.mdx @@ -24,6 +24,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + throw new Error('BROKEN'); // Express will catch this on its own. +}); +``` + For errors returned from asynchronous functions invoked by route handlers and middleware, you must pass them to the `next()` function, where Express will catch and process them. For example: @@ -40,6 +48,20 @@ app.get('/', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', (req: Request, res: Response, next: NextFunction) => { + fs.readFile('/file-does-not-exist', (err, data) => { + if (err) { + next(err); // Pass errors to Express. + } else { + res.send(data); + } + }); +}); +``` + Starting with Express 5, route handlers and middleware that return a Promise will call `next(value)` automatically when they reject or throw an error. For example: @@ -51,6 +73,15 @@ app.get('/user/:id', async (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/user/:id', async (req: Request, res: Response, next: NextFunction) => { + const user = await getUserById(req.params.id); + res.send(user); +}); +``` + If `getUserById` throws an error or rejects, `next` will be called with either the thrown error or the rejected value. If no rejected value is provided, `next` will be called with a default Error object provided by the Express router. @@ -73,6 +104,19 @@ app.get('/', [ ]); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', [ + function (req: Request, res: Response, next: NextFunction) { + fs.writeFile('/inaccessible-path', 'data', next); + }, + function (req: Request, res: Response) { + res.send('OK'); + }, +]); +``` + In the above example, `next` is provided as the callback for `fs.writeFile`, which is called with or without errors. If there is no error, the second handler is executed, otherwise Express catches and processes the error. @@ -92,6 +136,20 @@ app.get('/', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', (req: Request, res: Response, next: NextFunction) => { + setTimeout(() => { + try { + throw new Error('BROKEN'); + } catch (err) { + next(err); + } + }, 100); +}); +``` + The above example uses a `try...catch` block to catch errors in the asynchronous code and pass them to Express. If the `try...catch` block were omitted, Express would not catch the error since it is not part of the synchronous @@ -110,6 +168,18 @@ app.get('/', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', (req: Request, res: Response, next: NextFunction) => { + Promise.resolve() + .then(() => { + throw new Error('BROKEN'); + }) + .catch(next); // Errors will be passed to Express. +}); +``` + Since promises automatically catch both synchronous errors and rejected promises, you can simply provide `next` as the final catch handler and Express will catch errors, because the catch handler is given the error as the first argument. @@ -132,6 +202,23 @@ app.get('/', [ ]); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', [ + function (req: Request, res: Response, next: NextFunction) { + fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => { + res.locals.data = data; + next(err); + }); + }, + function (req: Request, res: Response) { + res.locals.data = res.locals.data.split(',')[1]; + res.send(res.locals.data); + }, +]); +``` + The above example has a couple of trivial statements from the `readFile` call. If `readFile` causes an error, then it passes the error to Express, otherwise you quickly return to the world of synchronous error handling in the next handler @@ -187,6 +274,18 @@ function errorHandler(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) { + if (res.headersSent) { + return next(err); + } + res.status(500); + res.render('error', { error: err }); +} +``` + Note that the default error handler can get triggered if you call `next()` with an error in your code more than once, even if custom error handling middleware is in place. @@ -205,6 +304,15 @@ app.use((err, req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + You define error-handling middleware last, after other `app.use()` and routes calls; for example: ```cjs title="index.cjs" @@ -239,6 +347,23 @@ app.use((err, req, res, next) => { }); ``` +```ts title="index.ts" +import bodyParser from 'body-parser'; +import methodOverride from 'method-override'; +import { type Request, type Response, type NextFunction } from 'express'; + +app.use( + bodyParser.urlencoded({ + extended: true, + }) +); +app.use(bodyParser.json()); +app.use(methodOverride()); +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + // logic +}); +``` + Responses from within a middleware function can be in any format, such as an HTML error page, a simple message, or a JSON string. For organizational (and higher-level framework) purposes, you can define @@ -288,6 +413,15 @@ function logErrors(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function logErrors(err: Error, req: Request, res: Response, next: NextFunction) { + console.error(err.stack); + next(err); +} +``` + Also in this example, `clientErrorHandler` is defined as follows; in this case, the error is explicitly passed along to the next one. Notice that when _not_ calling "next" in an error-handling function, you are responsible for writing (and ending) the response. Otherwise, those requests will "hang" and will not be eligible for garbage collection. @@ -302,6 +436,18 @@ function clientErrorHandler(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function clientErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) { + if (req.xhr) { + res.status(500).send({ error: 'Something failed!' }); + } else { + next(err); + } +} +``` + Implement the "catch-all" `errorHandler` function as follows (for example): ```js @@ -311,6 +457,15 @@ function errorHandler(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) { + res.status(500); + res.render('error', { error: err }); +} +``` + If you have a route handler with multiple callback functions, you can use the `route` parameter to skip to the next route handler. For example: ```js @@ -333,6 +488,28 @@ app.get( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/a_route_behind_paywall', + (req: Request, res: Response, next: NextFunction) => { + if (!req.user.hasPaid) { + // continue handling this request + next('route'); + } else { + next(); + } + }, + (req: Request, res: Response, next: NextFunction) => { + PaidContent.find((err, doc) => { + if (err) return next(err); + res.json(doc); + }); + } +); +``` + In this example, the `getPaidContent` handler will be skipped but any remaining handlers in `app` for `/a_route_behind_paywall` would continue to be executed. diff --git a/src/content/docs/en/4x/guide/overriding-express-api.md b/src/content/docs/en/4x/guide/overriding-express-api.md deleted file mode 100644 index ef06cf056c..0000000000 --- a/src/content/docs/en/4x/guide/overriding-express-api.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Overriding the Express API -description: Discover how to customize and extend the Express.js API by overriding methods and properties on the request and response objects using prototypes. ---- - -The Express API consists of various methods and properties on the request and response objects. These are inherited by prototype. There are two extension points for the Express API: - -1. The global prototypes at `express.request` and `express.response`. -2. App-specific prototypes at `app.request` and `app.response`. - -Altering the global prototypes will affect all loaded Express apps in the same process. If desired, alterations can be made app-specific by only altering the app-specific prototypes after creating a new app. - -## Methods - -You can override the signature and behavior of existing methods with your own, by assigning a custom function. - -Following is an example of overriding the behavior of [res.sendStatus](/api/response/#ressendstatus). - -```js -app.response.sendStatus = function (statusCode, type, message) { - // code is intentionally kept simple for demonstration purpose - return this.contentType(type).status(statusCode).send(message); -}; -``` - -The above implementation completely changes the original signature of `res.sendStatus`. It now accepts a status code, encoding type, and the message to be sent to the client. - -The overridden method may now be used this way: - -```js -res.sendStatus(404, 'application/json', '{"error":"resource not found"}'); -``` - -## Properties - -Properties in the Express API are either: - -1. Assigned properties (ex: `req.baseUrl`, `req.originalUrl`) -2. Defined as getters (ex: `req.secure`, `req.ip`) - -Since properties under category 1 are dynamically assigned on the `request` and `response` objects in the context of the current request-response cycle, their behavior cannot be overridden. - -Properties under category 2 can be overwritten using the Express API extensions API. - -The following code rewrites how the value of `req.ip` is to be derived. Now, it simply returns the value of the `Client-IP` request header. - -```js -Object.defineProperty(app.request, 'ip', { - configurable: true, - enumerable: true, - get() { - return this.get('Client-IP'); - }, -}); -``` - -## Prototype - -In order to provide the Express API, the request/response objects passed to Express (via `app(req, res)`, for example) need to inherit from the same prototype chain. By default, this is `http.IncomingRequest.prototype` for the request and `http.ServerResponse.prototype` for the response. - -Unless necessary, it is recommended that this be done only at the application level, rather than globally. Also, take care that the prototype that is being used matches the functionality as closely as possible to the default prototypes. - -```js -// Use FakeRequest and FakeResponse in place of http.IncomingRequest and http.ServerResponse -// for the given app reference -Object.setPrototypeOf(Object.getPrototypeOf(app.request), FakeRequest.prototype); -Object.setPrototypeOf(Object.getPrototypeOf(app.response), FakeResponse.prototype); -``` diff --git a/src/content/docs/en/4x/guide/overriding-express-api.mdx b/src/content/docs/en/4x/guide/overriding-express-api.mdx new file mode 100644 index 0000000000..54cef9edbc --- /dev/null +++ b/src/content/docs/en/4x/guide/overriding-express-api.mdx @@ -0,0 +1,170 @@ +--- +title: Overriding the Express API +description: Discover how to customize and extend the Express.js API by overriding methods and properties on the request and response objects using prototypes, including extending the API in TypeScript with declaration merging. +--- + +import Alert from '@components/primitives/Alert/Alert.astro'; + +The Express API consists of various methods and properties on the request and response objects. These are inherited by prototype. There are two extension points for the Express API: + +1. The global prototypes at `express.request` and `express.response`. +2. App-specific prototypes at `app.request` and `app.response`. + +Altering the global prototypes will affect all loaded Express apps in the same process. If desired, alterations can be made app-specific by only altering the app-specific prototypes after creating a new app. + +## Methods + +You can override the signature and behavior of existing methods with your own, by assigning a custom function. + +Following is an example of overriding the behavior of [res.sendStatus](/api/response/#ressendstatus). + +```js +app.response.sendStatus = function (statusCode, type, message) { + // code is intentionally kept simple for demonstration purpose + return this.contentType(type).status(statusCode).send(message); +}; +``` + +```ts +import { type Response } from 'express'; + +// Broaden the type of sendStatus so call sites accept the new arguments. +declare module 'express-serve-static-core' { + interface Response { + sendStatus(statusCode: number, type: string, message: string): this; + } +} + +app.response.sendStatus = function ( + this: Response, + statusCode: number, + type: string, + message: string +) { + // code is intentionally kept simple for demonstration purpose + return this.contentType(type).status(statusCode).send(message); +} as Response['sendStatus']; +``` + +The above implementation completely changes the original signature of `res.sendStatus`. It now accepts a status code, encoding type, and the message to be sent to the client. + +In TypeScript, augmenting `express-serve-static-core` adds the new overload to the `Response` type so call sites such as `res.sendStatus(404, 'application/json', body)` type-check. The assignment is cast with `as Response['sendStatus']` because replacing a method with a different signature cannot be checked against the original declaration. + +The overridden method may now be used this way: + +```js +res.sendStatus(404, 'application/json', '{"error":"resource not found"}'); +``` + +## Properties + +Properties in the Express API are either: + +1. Assigned properties (ex: `req.baseUrl`, `req.originalUrl`) +2. Defined as getters (ex: `req.secure`, `req.ip`) + +Since properties under category 1 are dynamically assigned on the `request` and `response` objects in the context of the current request-response cycle, their behavior cannot be overridden. + +Properties under category 2 can be overwritten using the Express API extensions API. + +The following code rewrites how the value of `req.ip` is to be derived. Now, it simply returns the value of the `Client-IP` request header. + +```js +Object.defineProperty(app.request, 'ip', { + configurable: true, + enumerable: true, + get() { + return this.get('Client-IP'); + }, +}); +``` + +```ts +import { type Request } from 'express'; + +Object.defineProperty(app.request, 'ip', { + configurable: true, + enumerable: true, + get(this: Request) { + return this.get('Client-IP'); + }, +}); +``` + +## Extending the API in TypeScript + +The sections above override members that Express already provides. To add your own properties or +methods to the request or response, describe them to TypeScript with +[declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) on the +`Express` namespace. Put the augmentation in a `.d.ts` file that is part of your project. No +`tsconfig.json` change is needed unless a custom `include` does not cover its location. + +For example, an authentication middleware may attach a `user` to the request, and you might add a +`sendError` helper to the response: + +```ts title="types/express.d.ts" +interface User { + id: string; + name: string; +} + +declare global { + namespace Express { + interface Request { + user?: User; + } + interface Response { + sendError(status: number, message: string): this; + } + } +} + +export {}; +``` + +The additions are now known throughout the application: + +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + req.user = { id: '1', name: 'Tobi' }; + next(); +}); + +app.response.sendError = function (this: Response, status: number, message: string) { + return this.status(status).json({ error: message }); +}; + +app.get('/', (req: Request, res: Response) => { + if (!req.user) { + res.sendError(401, 'unauthorized'); + return; + } + res.send(req.user.name); +}); +``` + + + +Declare custom request properties as optional (`user?`). The type applies to every request, but +TypeScript cannot know which middleware ran before a given handler, so a required property would be a +false guarantee. Check for the value (as with `if (!req.user)` above) before relying on it. + + + +Adding a new method needs no cast, unlike [overriding](#methods) an existing method with a different +signature, because the method did not previously exist on the type. + +## Prototype + +In order to provide the Express API, the request/response objects passed to Express (via `app(req, res)`, for example) need to inherit from the same prototype chain. By default, this is `http.IncomingRequest.prototype` for the request and `http.ServerResponse.prototype` for the response. + +Unless necessary, it is recommended that this be done only at the application level, rather than globally. Also, take care that the prototype that is being used matches the functionality as closely as possible to the default prototypes. + +```js +// Use FakeRequest and FakeResponse in place of http.IncomingRequest and http.ServerResponse +// for the given app reference +Object.setPrototypeOf(Object.getPrototypeOf(app.request), FakeRequest.prototype); +Object.setPrototypeOf(Object.getPrototypeOf(app.response), FakeResponse.prototype); +``` diff --git a/src/content/docs/en/4x/guide/routing.mdx b/src/content/docs/en/4x/guide/routing.mdx index ccaf7c6c6b..3924d9e7f9 100644 --- a/src/content/docs/en/4x/guide/routing.mdx +++ b/src/content/docs/en/4x/guide/routing.mdx @@ -1,6 +1,6 @@ --- title: Routing -description: Learn how to define and use routes in Express.js applications, including route methods, route paths, parameters, and using Router for modular routing. +description: Learn how to define and use routes in Express.js applications, including route methods, route paths, parameters, typing requests and responses in TypeScript, and using Router for modular routing. --- import Alert from '@components/primitives/Alert/Alert.astro'; @@ -42,6 +42,17 @@ app.get('/', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// respond with "hello world" when a GET request is made to the homepage +app.get('/', (req: Request, res: Response) => { + res.send('hello world'); +}); +``` + ## Route methods A route method is derived from one of the HTTP methods, and is attached to an instance of the `express` class. @@ -60,6 +71,20 @@ app.post('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +// GET method route +app.get('/', (req: Request, res: Response) => { + res.send('GET request to the homepage'); +}); + +// POST method route +app.post('/', (req: Request, res: Response) => { + res.send('POST request to the homepage'); +}); +``` + Express supports methods that correspond to all HTTP request methods: `get`, `post`, and so on. For a full list, see [app.METHOD](/api/application#appmethod). @@ -72,6 +97,15 @@ app.all('/secret', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.all('/secret', (req: Request, res: Response, next: NextFunction) => { + console.log('Accessing the secret section ...'); + next(); // pass control to the next handler +}); +``` + ## Route paths Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expressions. @@ -95,6 +129,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.send('root'); +}); +``` + This route path will match requests to `/about`. ```js @@ -103,6 +145,14 @@ app.get('/about', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/about', (req: Request, res: Response) => { + res.send('about'); +}); +``` + This route path will match requests to `/random.text`. ```js @@ -111,6 +161,14 @@ app.get('/random.text', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/random.text', (req: Request, res: Response) => { + res.send('random.text'); +}); +``` + ### Route paths based on string patterns This route path will match `acd` and `abcd`. @@ -121,6 +179,14 @@ app.get('/ab?cd', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/ab?cd', (req: Request, res: Response) => { + res.send('ab?cd'); +}); +``` + This route path will match `abcd`, `abbcd`, `abbbcd`, and so on. ```js @@ -129,6 +195,14 @@ app.get('/ab+cd', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/ab+cd', (req: Request, res: Response) => { + res.send('ab+cd'); +}); +``` + This route path will match `abcd`, `abxcd`, `abRANDOMcd`, `ab123cd`, and so on. ```js @@ -137,6 +211,14 @@ app.get('/ab*cd', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/ab*cd', (req: Request, res: Response) => { + res.send('ab*cd'); +}); +``` + This route path will match `/abe` and `/abcde`. ```js @@ -145,6 +227,14 @@ app.get('/ab(cd)?e', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/ab(cd)?e', (req: Request, res: Response) => { + res.send('ab(cd)?e'); +}); +``` + ### Route paths based on regular expressions @@ -161,6 +251,14 @@ app.get(/a/, (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get(/a/, (req: Request, res: Response) => { + res.send('/a/'); +}); +``` + This route path will match `butterfly` and `dragonfly`, but not `butterflyman`, `dragonflyman`, and so on. ```js @@ -169,6 +267,14 @@ app.get(/.*fly$/, (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get(/.*fly$/, (req: Request, res: Response) => { + res.send('/.*fly$/'); +}); +``` + ## Route parameters Route parameters are named URL segments that are used to capture the values specified at their position in the URL. The captured values are populated in the `req.params` object, with the name of the route parameter specified in the path as their respective keys. @@ -187,6 +293,22 @@ app.get('/users/:userId/books/:bookId', (req, res) => { }); ``` +In TypeScript, `@types/express` infers the parameters from the route path, so in the handler above +`req.params.userId` and `req.params.bookId` are already typed as `string` with no extra annotation. +Reading a name that is not in the route (such as `req.params.other`) is a type error. You only need +to annotate the parameters when the handler is defined separately from the route, because the type +checker can no longer see the path. In that case, pass them as the first type argument of `Request`: + +```ts +import { type Request, type Response } from 'express'; + +const sendParams = (req: Request<{ userId: string; bookId: string }>, res: Response) => { + res.send(req.params); +}; + +app.get('/users/:userId/books/:bookId', sendParams); +``` + The name of route parameters must be made up of "word characters" ([A-Za-z0-9_]). @@ -243,6 +365,21 @@ app.get('/user/:id', (req, res) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + if (req.params.id === '0') { + return next('route'); + } + res.send(`User ${req.params.id}`); +}); + +app.get('/user/:id', (req: Request, res: Response) => { + res.send('Special handler for user ID 0'); +}); +``` + In this example: - `GET /user/5` → handled by first route → sends "User 5" @@ -258,6 +395,14 @@ app.get('/example/a', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/example/a', (req: Request, res: Response) => { + res.send('Hello from A!'); +}); +``` + More than one callback function can handle a route (make sure you specify the `next` object). For example: ```js @@ -273,6 +418,21 @@ app.get( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/example/b', + (req: Request, res: Response, next: NextFunction) => { + console.log('the response will be sent by the next function ...'); + next(); + }, + (req: Request, res: Response) => { + res.send('Hello from B!'); + } +); +``` + An array of callback functions can handle a route. For example: ```js @@ -293,6 +453,26 @@ const cb2 = function (req, res) { app.get('/example/c', [cb0, cb1, cb2]); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const cb0 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB0'); + next(); +}; + +const cb1 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB1'); + next(); +}; + +const cb2 = function (req: Request, res: Response) { + res.send('Hello from C!'); +}; + +app.get('/example/c', [cb0, cb1, cb2]); +``` + A combination of independent functions and arrays of functions can handle a route. For example: ```js @@ -319,6 +499,32 @@ app.get( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const cb0 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB0'); + next(); +}; + +const cb1 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB1'); + next(); +}; + +app.get( + '/example/d', + [cb0, cb1], + (req: Request, res: Response, next: NextFunction) => { + console.log('the response will be sent by the next function ...'); + next(); + }, + (req: Request, res: Response) => { + res.send('Hello from D!'); + } +); +``` + ## Response methods The methods on the response object (`res`) in the following table can send a response to the client, and terminate the request-response cycle. If none of these methods are called from a route handler, the client request will be left hanging. @@ -356,6 +562,22 @@ app }); ``` +```ts +import { type Request, type Response } from 'express'; + +app + .route('/book') + .get((req: Request, res: Response) => { + res.send('Get a random book'); + }) + .post((req: Request, res: Response) => { + res.send('Add a book'); + }) + .put((req: Request, res: Response) => { + res.send('Update the book'); + }); +``` + ## express.Router Use the `express.Router` class to create modular, mountable route handlers. A `Router` instance is a complete middleware and routing system; for this reason, it is often referred to as a "mini-app". @@ -411,6 +633,30 @@ router.get('/about', (req, res) => { export default router; ``` +```ts title="birds.ts" +import express, { type Request, type Response, type NextFunction } from 'express'; + +const router = express.Router(); + +// middleware that is specific to this router +const timeLog = (req: Request, res: Response, next: NextFunction) => { + console.log('Time: ', Date.now()); + next(); +}; +router.use(timeLog); + +// define the home page route +router.get('/', (req: Request, res: Response) => { + res.send('Birds home page'); +}); +// define the about route +router.get('/about', (req: Request, res: Response) => { + res.send('About birds'); +}); + +export default router; +``` + Then, load the router module in the app: ```cjs title="index.cjs" diff --git a/src/content/docs/en/4x/guide/using-middleware.mdx b/src/content/docs/en/4x/guide/using-middleware.mdx index 77723e2f37..987633d120 100644 --- a/src/content/docs/en/4x/guide/using-middleware.mdx +++ b/src/content/docs/en/4x/guide/using-middleware.mdx @@ -57,6 +57,17 @@ app.use((req, res, next) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +app.use((req: Request, res: Response, next: NextFunction) => { + console.log('Time:', Date.now()); + next(); +}); +``` + This example shows a middleware function mounted on the `/user/:id` path. The function is executed for any type of HTTP request on the `/user/:id` path. @@ -67,6 +78,15 @@ app.use('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/user/:id', (req: Request, res: Response, next: NextFunction) => { + console.log('Request Type:', req.method); + next(); +}); +``` + This example shows a route and its handler function (middleware system). The function handles GET requests to the `/user/:id` path. ```js @@ -75,6 +95,14 @@ app.get('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + res.send('USER'); +}); +``` + Here is an example of loading a series of middleware functions at a mount point, with a mount path. It illustrates a middleware sub-stack that prints request info for any type of HTTP request to the `/user/:id` path. @@ -92,6 +120,22 @@ app.use( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + console.log('Request URL:', req.originalUrl); + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + console.log('Request Type:', req.method); + next(); + } +); +``` + Route handlers enable you to define multiple routes for a path. The example below defines two routes for GET requests to the `/user/:id` path. The second route will not cause any problems, but it will never get called because the first route ends the request-response cycle. This example shows a middleware sub-stack that handles GET requests to the `/user/:id` path. @@ -114,6 +158,26 @@ app.get('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + console.log('ID:', req.params.id); + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + res.send('User Info'); + } +); + +// handler for the /user/:id path, which prints the user ID +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + res.send(req.params.id); +}); +``` + To skip the rest of the middleware functions from a router middleware stack, call `next('route')` to pass control to the next route. @@ -146,6 +210,29 @@ app.get('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + // if the user ID is 0, skip to the next route + if (req.params.id === '0') next('route'); + // otherwise pass the control to the next middleware function in this stack + else next(); + }, + (req: Request, res: Response, next: NextFunction) => { + // send a regular response + res.send('regular'); + } +); + +// handler for the /user/:id path, which sends a special response +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + res.send('special'); +}); +``` + Middleware can also be declared in an array for reusability. This example shows an array with a middleware sub-stack that handles GET requests to the `/user/:id` path @@ -167,6 +254,25 @@ app.get('/user/:id', logStuff, (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function logOriginalUrl(req: Request, res: Response, next: NextFunction) { + console.log('Request URL:', req.originalUrl); + next(); +} + +function logMethod(req: Request, res: Response, next: NextFunction) { + console.log('Request Type:', req.method); + next(); +} + +const logStuff = [logOriginalUrl, logMethod]; +app.get('/user/:id', logStuff, (req: Request, res: Response, next: NextFunction) => { + res.send('User Info'); +}); +``` + ## Router-level middleware Router-level middleware works in the same way as application-level middleware, except it is bound to an instance of `express.Router()`. @@ -278,6 +384,56 @@ router.get('/user/:id', (req, res, next) => { app.use('/', router); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); +const router = express.Router(); + +// a middleware function with no mount path. This code is executed for every request to the router +router.use((req: Request, res: Response, next: NextFunction) => { + console.log('Time:', Date.now()); + next(); +}); + +// a middleware sub-stack shows request info for any type of HTTP request to the /user/:id path +router.use( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + console.log('Request URL:', req.originalUrl); + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + console.log('Request Type:', req.method); + next(); + } +); + +// a middleware sub-stack that handles GET requests to the /user/:id path +router.get( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + // if the user ID is 0, skip to the next router + if (req.params.id === '0') next('route'); + // otherwise pass control to the next middleware function in this stack + else next(); + }, + (req: Request, res: Response, next: NextFunction) => { + // render a regular page + res.render('regular'); + } +); + +// handler for the /user/:id path, which renders a special page +router.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + console.log(req.params.id); + res.render('special'); +}); + +// mount the router on the app +app.use('/', router); +``` + To skip the rest of the router's middleware functions, call `next('router')` to pass control back out of the router instance. @@ -326,6 +482,28 @@ app.use('/admin', router, (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); +const router = express.Router(); + +// predicate the router with a check and bail out when needed +router.use((req: Request, res: Response, next: NextFunction) => { + if (!req.headers['x-auth']) return next('router'); + next(); +}); + +router.get('/user/:id', (req: Request, res: Response) => { + res.send('hello, user!'); +}); + +// use the router and 401 anything falling through +app.use('/admin', router, (req: Request, res: Response) => { + res.sendStatus(401); +}); +``` + ## Error-handling middleware @@ -346,6 +524,15 @@ app.use((err, req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + For details about error-handling middleware, see: [Error handling](/guide/error-handling). ## Built-in middleware @@ -388,4 +575,14 @@ const app = express(); app.use(cookieParser()); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; +import cookieParser from 'cookie-parser'; + +const app: Express = express(); + +// load the cookie-parsing middleware +app.use(cookieParser()); +``` + For a partial list of third-party middleware functions that are commonly used with Express, see: [Third-party middleware](/resources/middleware). diff --git a/src/content/docs/en/4x/guide/using-template-engines.mdx b/src/content/docs/en/4x/guide/using-template-engines.mdx index 18de44b3bf..18284a627d 100644 --- a/src/content/docs/en/4x/guide/using-template-engines.mdx +++ b/src/content/docs/en/4x/guide/using-template-engines.mdx @@ -57,6 +57,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.render('index', { title: 'Hey', message: 'Hello there!' }); +}); +``` + When you make a request to the home page, the `index.pug` file will be rendered as HTML. The view engine cache does not cache the contents of the template's output, only the underlying template itself. The view is still re-rendered with every request even when the cache is on. diff --git a/src/content/docs/en/4x/guide/writing-middleware.mdx b/src/content/docs/en/4x/guide/writing-middleware.mdx index 9dedb5889e..5c36b01a72 100644 --- a/src/content/docs/en/4x/guide/writing-middleware.mdx +++ b/src/content/docs/en/4x/guide/writing-middleware.mdx @@ -79,6 +79,18 @@ app.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(3000); +``` + ### Middleware function myLogger Here is a simple example of a middleware function called "myLogger". This function just prints @@ -92,6 +104,21 @@ const myLogger = function (req, res, next) { }; ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const myLogger = function (req: Request, res: Response, next: NextFunction) { + console.log('LOGGED'); + next(); +}; +``` + +Here `myLogger` is defined on its own rather than passed directly to `app.use()`, so TypeScript has +no context to infer its parameters and they are annotated explicitly. You can instead type the whole +function as `RequestHandler`, which types `req`, `res`, and `next` for you. When the middleware is +written inline in the `app.use()` call, Express infers those three parameters and no annotations are +needed. + Notice the call above to `next()`. Calling this function invokes the next middleware function in @@ -141,6 +168,25 @@ app.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +const myLogger = function (req: Request, res: Response, next: NextFunction) { + console.log('LOGGED'); + next(); +}; + +app.use(myLogger); + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(3000); +``` + Every time the app receives a request, it prints the message "LOGGED" to the terminal. The order of middleware loading is important: middleware functions that are loaded first are also executed first. @@ -154,6 +200,32 @@ The middleware function `myLogger` simply prints a message, then passes on the r Next, we'll create a middleware function called "requestTime" and add a property called `requestTime` to the request object. + + +Because the middleware adds a property to `req`, extend the request type in TypeScript with +[declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html). Declare +the property (as optional, since a middleware may not run for every request) in a `.d.ts` file that +is part of your project: + +```ts title="types/express.d.ts" +declare global { + namespace Express { + interface Request { + requestTime?: number; + } + } +} + +export {}; +``` + +TypeScript picks up the file automatically, so no `tsconfig.json` change is needed unless you have +set a custom `include` that does not cover its location. See +[Extending the API in TypeScript](/guide/overriding-express-api#extending-the-api-in-typescript) for +adding other custom properties and methods. + + + ```js const requestTime = function (req, res, next) { req.requestTime = Date.now(); @@ -161,6 +233,15 @@ const requestTime = function (req, res, next) { }; ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const requestTime = function (req: Request, res: Response, next: NextFunction) { + req.requestTime = Date.now(); + next(); +}; +``` + The app now uses the `requestTime` middleware function. Also, the callback function of the root path route uses the property that the middleware function adds to `req` (the request object). ```cjs title="index.cjs" @@ -204,6 +285,27 @@ app.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +const requestTime = function (req: Request, res: Response, next: NextFunction) { + req.requestTime = Date.now(); + next(); +}; + +app.use(requestTime); + +app.get('/', (req: Request, res: Response) => { + let responseText = 'Hello World!
'; + responseText += `Requested at: ${req.requestTime}`; + res.send(responseText); +}); + +app.listen(3000); +``` + When you make a request to the root of the app, the app now displays the timestamp of your request in the browser. ### Middleware function validateCookies @@ -222,6 +324,16 @@ async function cookieValidator(cookies) { } ``` +```ts +async function cookieValidator(cookies: Record) { + try { + await externallyValidateCookie(cookies.testCookie); + } catch { + throw new Error('Invalid cookies'); + } +} +``` + Here, we use the [`cookie-parser`](/resources/middleware/cookie-parser) middleware to parse incoming cookies off the `req` object and pass them to our `cookieValidator` function. The `validateCookies` middleware returns a Promise that upon rejection will automatically trigger our error handler. ```cjs title="index.cjs" @@ -272,6 +384,30 @@ app.use((err, req, res, next) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; +import cookieParser from 'cookie-parser'; +import cookieValidator from './cookieValidator'; + +const app: Express = express(); + +async function validateCookies(req: Request, res: Response, next: NextFunction) { + await cookieValidator(req.cookies); + next(); +} + +app.use(cookieParser()); + +app.use(validateCookies); + +// error handler +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + res.status(400).send(err.message); +}); + +app.listen(3000); +``` + Note how `next()` is called after `await cookieValidator(req.cookies)`. This ensures that if @@ -308,6 +444,17 @@ export default function (options) { } ``` +```ts title="my-middleware.ts" +import { type Request, type Response, type NextFunction } from 'express'; + +export default function (options) { + return function (req: Request, res: Response, next: NextFunction) { + // Implement the middleware function based on the options object + next(); + }; +} +``` + The middleware can now be used as shown below. ```cjs title="index.cjs" @@ -322,4 +469,10 @@ import mw from './my-middleware.mjs'; app.use(mw({ option1: '1', option2: '2' })); ``` +```ts title="index.ts" +import mw from './my-middleware'; + +app.use(mw({ option1: '1', option2: '2' })); +``` + Refer to [cookie-session](https://github.com/expressjs/cookie-session) and [compression](https://github.com/expressjs/compression) for examples of configurable middleware. diff --git a/src/content/docs/en/4x/starter/basic-routing.mdx b/src/content/docs/en/4x/starter/basic-routing.mdx index 7751b027a6..2b78e00490 100644 --- a/src/content/docs/en/4x/starter/basic-routing.mdx +++ b/src/content/docs/en/4x/starter/basic-routing.mdx @@ -40,6 +40,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); +``` + Respond to a POST request on the root route (`/`), the application's home page: ```js @@ -48,6 +56,14 @@ app.post('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.post('/', (req: Request, res: Response) => { + res.send('Got a POST request'); +}); +``` + Respond to a PUT request to the `/user` route: ```js @@ -56,6 +72,14 @@ app.put('/user', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.put('/user', (req: Request, res: Response) => { + res.send('Got a PUT request at /user'); +}); +``` + Respond to a DELETE request to the `/user` route: ```js @@ -64,4 +88,12 @@ app.delete('/user', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.delete('/user', (req: Request, res: Response) => { + res.send('Got a DELETE request at /user'); +}); +``` + For more details about routing, see the [routing guide](/guide/routing). diff --git a/src/content/docs/en/4x/starter/faq.md b/src/content/docs/en/4x/starter/faq.mdx similarity index 89% rename from src/content/docs/en/4x/starter/faq.md rename to src/content/docs/en/4x/starter/faq.mdx index 98227f77fd..16863093e0 100755 --- a/src/content/docs/en/4x/starter/faq.md +++ b/src/content/docs/en/4x/starter/faq.mdx @@ -60,6 +60,14 @@ app.use((req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + res.status(404).send("Sorry can't find that!"); +}); +``` + Add routes dynamically at runtime on an instance of `express.Router()` so the routes are not superseded by a middleware function. @@ -75,6 +83,15 @@ app.use((err, req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + For more information, see [Error handling](/guide/error-handling). ## How do I render plain HTML? diff --git a/src/content/docs/en/4x/starter/hello-world.mdx b/src/content/docs/en/4x/starter/hello-world.mdx index 4c26ad0813..faed0615b2 100644 --- a/src/content/docs/en/4x/starter/hello-world.mdx +++ b/src/content/docs/en/4x/starter/hello-world.mdx @@ -43,6 +43,21 @@ app.listen(port, () => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); +const port = 3000; + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); +``` + This app starts a server and listens on port 3000 for connections. The app responds with "Hello World!" for requests to the root URL (`/`) or _route_. For every other path, it will respond with a **404 Not Found**. diff --git a/src/content/docs/en/4x/starter/installing.mdx b/src/content/docs/en/4x/starter/installing.mdx index d2e6ad2a1a..2b040edf10 100644 --- a/src/content/docs/en/4x/starter/installing.mdx +++ b/src/content/docs/en/4x/starter/installing.mdx @@ -1,6 +1,6 @@ --- title: Installing -description: Learn how to install Express.js in your Node.js environment, including setting up your project directory and managing dependencies with npm. +description: Learn how to install Express.js in your Node.js environment, including setting up your project directory, managing dependencies with npm, and configuring TypeScript. --- import Alert from '@components/primitives/Alert/Alert.astro'; @@ -43,3 +43,76 @@ explicitly. Then, afterwards, running `npm install` in the app directory will au install modules in the dependencies list. + +## TypeScript + +Express is written in JavaScript and does not bundle its own type definitions. To use it with +TypeScript, install TypeScript together with the community-maintained types for Express and Node.js +(from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)) as development +dependencies: + + + + + +Some middleware does not bundle its own type definitions. If you add an official middleware package +that TypeScript reports as untyped, also install its types from DefinitelyTyped as a dev dependency, +for example `@types/cors` alongside `cors`. + + + +Add a `tsconfig.json`. These options mirror how Node.js runs TypeScript and make the compiler reject +non-erasable syntax (such as `enum`s, namespaces, and parameter properties) that Node cannot strip: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "esnext", + "module": "nodenext", + "rewriteRelativeImportExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true + } +} +``` + +Write your application in TypeScript, annotating the request and response objects: + +```ts title="src/app.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(3000); +``` + +You do not need to annotate everything. When you pass a handler directly to a route method or to +`app.use()`, Express infers the types of `req`, `res`, and `next`, and it infers route parameters +from the path, so `req.params.id` is a `string` in `app.get('/users/:id', ...)`. Add explicit types +only where TypeScript has no context to infer from: error-handling middleware, whose +`(err, req, res, next)` signature is not inferred, and handlers you define separately from the route. +In those cases, annotate the parameters or type the whole function as `RequestHandler` or +`ErrorRequestHandler`. + +Run the file directly with Node.js, which strips the TypeScript types and runs the result without a +build step: + +```bash +node src/app.ts +``` + + + +Running `.ts` files directly requires Node.js >= 22.18.0 (or >= 23.6.0 on the v23 line) and +TypeScript >= 5.8. Node strips the types but does not type-check them, so run `npx tsc` to type-check +your project. For more details, see the Node.js guide on +[running TypeScript natively](https://nodejs.org/learn/typescript/run-natively). + + diff --git a/src/content/docs/en/5x/advanced/developing-template-engines.md b/src/content/docs/en/5x/advanced/developing-template-engines.mdx similarity index 69% rename from src/content/docs/en/5x/advanced/developing-template-engines.md rename to src/content/docs/en/5x/advanced/developing-template-engines.mdx index 6e9710fbc5..bf5ae9b2f1 100755 --- a/src/content/docs/en/5x/advanced/developing-template-engines.md +++ b/src/content/docs/en/5x/advanced/developing-template-engines.mdx @@ -25,6 +25,31 @@ app.set('views', './views'); // specify the views directory app.set('view engine', 'ntl'); // register the template engine ``` +```ts +import fs from 'fs'; // this engine requires the fs module +app.engine( + 'ntl', + ( + filePath: string, + options: Record, + callback: (e: any, rendered?: string) => void + ) => { + // define the template engine + fs.readFile(filePath, (err, content) => { + if (err) return callback(err); + // this is an extremely simple template engine + const rendered = content + .toString() + .replace('#title#', `${options.title}`) + .replace('#message#', `

${options.message}

`); + return callback(null, rendered); + }); + } +); +app.set('views', './views'); // specify the views directory +app.set('view engine', 'ntl'); // register the template engine +``` + Your app will now be able to render `.ntl` files. Create a file named `index.ntl` in the `views` directory with the following content. ```pug diff --git a/src/content/docs/en/5x/guide/behind-proxies.mdx b/src/content/docs/en/5x/guide/behind-proxies.mdx index bc2e774077..dd1f1d00df 100644 --- a/src/content/docs/en/5x/guide/behind-proxies.mdx +++ b/src/content/docs/en/5x/guide/behind-proxies.mdx @@ -77,6 +77,14 @@ app.set('trust proxy', (ip) => { }); ``` +```ts +app.set('trust proxy', (ip: string) => { + if (ip === '127.0.0.1' || ip === '123.123.123.123') + return true; // trusted IPs + else return false; +}); +``` + diff --git a/src/content/docs/en/5x/guide/error-handling.mdx b/src/content/docs/en/5x/guide/error-handling.mdx index 3d65d4d3b6..f933d9e32b 100644 --- a/src/content/docs/en/5x/guide/error-handling.mdx +++ b/src/content/docs/en/5x/guide/error-handling.mdx @@ -24,6 +24,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + throw new Error('BROKEN'); // Express will catch this on its own. +}); +``` + For errors returned from asynchronous functions invoked by route handlers and middleware, you must pass them to the `next()` function, where Express will catch and process them. For example: @@ -40,6 +48,20 @@ app.get('/', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', (req: Request, res: Response, next: NextFunction) => { + fs.readFile('/file-does-not-exist', (err, data) => { + if (err) { + next(err); // Pass errors to Express. + } else { + res.send(data); + } + }); +}); +``` + Starting with Express 5, route handlers and middleware that return a Promise will call `next(value)` automatically when they reject or throw an error. For example: @@ -51,6 +73,15 @@ app.get('/user/:id', async (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/user/:id', async (req: Request, res: Response, next: NextFunction) => { + const user = await getUserById(req.params.id); + res.send(user); +}); +``` + If `getUserById` throws an error or rejects, `next` will be called with either the thrown error or the rejected value. If no rejected value is provided, `next` will be called with a default Error object provided by the Express router. @@ -73,6 +104,19 @@ app.get('/', [ ]); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', [ + function (req: Request, res: Response, next: NextFunction) { + fs.writeFile('/inaccessible-path', 'data', next); + }, + function (req: Request, res: Response) { + res.send('OK'); + }, +]); +``` + In the above example, `next` is provided as the callback for `fs.writeFile`, which is called with or without errors. If there is no error, the second handler is executed, otherwise Express catches and processes the error. @@ -92,6 +136,20 @@ app.get('/', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', (req: Request, res: Response, next: NextFunction) => { + setTimeout(() => { + try { + throw new Error('BROKEN'); + } catch (err) { + next(err); + } + }, 100); +}); +``` + The above example uses a `try...catch` block to catch errors in the asynchronous code and pass them to Express. If the `try...catch` block were omitted, Express would not catch the error since it is not part of the synchronous @@ -110,6 +168,18 @@ app.get('/', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', (req: Request, res: Response, next: NextFunction) => { + Promise.resolve() + .then(() => { + throw new Error('BROKEN'); + }) + .catch(next); // Errors will be passed to Express. +}); +``` + Since promises automatically catch both synchronous errors and rejected promises, you can simply provide `next` as the final catch handler and Express will catch errors, because the catch handler is given the error as the first argument. @@ -132,6 +202,23 @@ app.get('/', [ ]); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/', [ + function (req: Request, res: Response, next: NextFunction) { + fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => { + res.locals.data = data; + next(err); + }); + }, + function (req: Request, res: Response) { + res.locals.data = res.locals.data.split(',')[1]; + res.send(res.locals.data); + }, +]); +``` + The above example has a couple of trivial statements from the `readFile` call. If `readFile` causes an error, then it passes the error to Express, otherwise you quickly return to the world of synchronous error handling in the next handler @@ -187,6 +274,18 @@ function errorHandler(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) { + if (res.headersSent) { + return next(err); + } + res.status(500); + res.render('error', { error: err }); +} +``` + Note that the default error handler can get triggered if you call `next()` with an error in your code more than once, even if custom error handling middleware is in place. @@ -205,6 +304,15 @@ app.use((err, req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + You define error-handling middleware last, after other `app.use()` and routes calls; for example: ```cjs title="index.cjs" @@ -239,6 +347,23 @@ app.use((err, req, res, next) => { }); ``` +```ts title="index.ts" +import { type Request, type Response, type NextFunction } from 'express'; +import bodyParser from 'body-parser'; +import methodOverride from 'method-override'; + +app.use( + bodyParser.urlencoded({ + extended: true, + }) +); +app.use(bodyParser.json()); +app.use(methodOverride()); +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + // logic +}); +``` + Responses from within a middleware function can be in any format, such as an HTML error page, a simple message, or a JSON string. For organizational (and higher-level framework) purposes, you can define @@ -288,6 +413,15 @@ function logErrors(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function logErrors(err: Error, req: Request, res: Response, next: NextFunction) { + console.error(err.stack); + next(err); +} +``` + Also in this example, `clientErrorHandler` is defined as follows; in this case, the error is explicitly passed along to the next one. Notice that when _not_ calling "next" in an error-handling function, you are responsible for writing (and ending) the response. Otherwise, those requests will "hang" and will not be eligible for garbage collection. @@ -302,6 +436,18 @@ function clientErrorHandler(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function clientErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) { + if (req.xhr) { + res.status(500).send({ error: 'Something failed!' }); + } else { + next(err); + } +} +``` + Implement the "catch-all" `errorHandler` function as follows (for example): ```js @@ -311,6 +457,15 @@ function errorHandler(err, req, res, next) { } ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) { + res.status(500); + res.render('error', { error: err }); +} +``` + If you have a route handler with multiple callback functions, you can use the `route` parameter to skip to the next route handler. For example: ```js @@ -333,6 +488,28 @@ app.get( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/a_route_behind_paywall', + (req: Request, res: Response, next: NextFunction) => { + if (!req.user.hasPaid) { + // continue handling this request + next('route'); + } else { + next(); + } + }, + (req: Request, res: Response, next: NextFunction) => { + PaidContent.find((err, doc) => { + if (err) return next(err); + res.json(doc); + }); + } +); +``` + In this example, the `getPaidContent` handler will be skipped but any remaining handlers in `app` for `/a_route_behind_paywall` would continue to be executed. diff --git a/src/content/docs/en/5x/guide/overriding-express-api.md b/src/content/docs/en/5x/guide/overriding-express-api.md deleted file mode 100644 index ef06cf056c..0000000000 --- a/src/content/docs/en/5x/guide/overriding-express-api.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Overriding the Express API -description: Discover how to customize and extend the Express.js API by overriding methods and properties on the request and response objects using prototypes. ---- - -The Express API consists of various methods and properties on the request and response objects. These are inherited by prototype. There are two extension points for the Express API: - -1. The global prototypes at `express.request` and `express.response`. -2. App-specific prototypes at `app.request` and `app.response`. - -Altering the global prototypes will affect all loaded Express apps in the same process. If desired, alterations can be made app-specific by only altering the app-specific prototypes after creating a new app. - -## Methods - -You can override the signature and behavior of existing methods with your own, by assigning a custom function. - -Following is an example of overriding the behavior of [res.sendStatus](/api/response/#ressendstatus). - -```js -app.response.sendStatus = function (statusCode, type, message) { - // code is intentionally kept simple for demonstration purpose - return this.contentType(type).status(statusCode).send(message); -}; -``` - -The above implementation completely changes the original signature of `res.sendStatus`. It now accepts a status code, encoding type, and the message to be sent to the client. - -The overridden method may now be used this way: - -```js -res.sendStatus(404, 'application/json', '{"error":"resource not found"}'); -``` - -## Properties - -Properties in the Express API are either: - -1. Assigned properties (ex: `req.baseUrl`, `req.originalUrl`) -2. Defined as getters (ex: `req.secure`, `req.ip`) - -Since properties under category 1 are dynamically assigned on the `request` and `response` objects in the context of the current request-response cycle, their behavior cannot be overridden. - -Properties under category 2 can be overwritten using the Express API extensions API. - -The following code rewrites how the value of `req.ip` is to be derived. Now, it simply returns the value of the `Client-IP` request header. - -```js -Object.defineProperty(app.request, 'ip', { - configurable: true, - enumerable: true, - get() { - return this.get('Client-IP'); - }, -}); -``` - -## Prototype - -In order to provide the Express API, the request/response objects passed to Express (via `app(req, res)`, for example) need to inherit from the same prototype chain. By default, this is `http.IncomingRequest.prototype` for the request and `http.ServerResponse.prototype` for the response. - -Unless necessary, it is recommended that this be done only at the application level, rather than globally. Also, take care that the prototype that is being used matches the functionality as closely as possible to the default prototypes. - -```js -// Use FakeRequest and FakeResponse in place of http.IncomingRequest and http.ServerResponse -// for the given app reference -Object.setPrototypeOf(Object.getPrototypeOf(app.request), FakeRequest.prototype); -Object.setPrototypeOf(Object.getPrototypeOf(app.response), FakeResponse.prototype); -``` diff --git a/src/content/docs/en/5x/guide/overriding-express-api.mdx b/src/content/docs/en/5x/guide/overriding-express-api.mdx new file mode 100644 index 0000000000..54cef9edbc --- /dev/null +++ b/src/content/docs/en/5x/guide/overriding-express-api.mdx @@ -0,0 +1,170 @@ +--- +title: Overriding the Express API +description: Discover how to customize and extend the Express.js API by overriding methods and properties on the request and response objects using prototypes, including extending the API in TypeScript with declaration merging. +--- + +import Alert from '@components/primitives/Alert/Alert.astro'; + +The Express API consists of various methods and properties on the request and response objects. These are inherited by prototype. There are two extension points for the Express API: + +1. The global prototypes at `express.request` and `express.response`. +2. App-specific prototypes at `app.request` and `app.response`. + +Altering the global prototypes will affect all loaded Express apps in the same process. If desired, alterations can be made app-specific by only altering the app-specific prototypes after creating a new app. + +## Methods + +You can override the signature and behavior of existing methods with your own, by assigning a custom function. + +Following is an example of overriding the behavior of [res.sendStatus](/api/response/#ressendstatus). + +```js +app.response.sendStatus = function (statusCode, type, message) { + // code is intentionally kept simple for demonstration purpose + return this.contentType(type).status(statusCode).send(message); +}; +``` + +```ts +import { type Response } from 'express'; + +// Broaden the type of sendStatus so call sites accept the new arguments. +declare module 'express-serve-static-core' { + interface Response { + sendStatus(statusCode: number, type: string, message: string): this; + } +} + +app.response.sendStatus = function ( + this: Response, + statusCode: number, + type: string, + message: string +) { + // code is intentionally kept simple for demonstration purpose + return this.contentType(type).status(statusCode).send(message); +} as Response['sendStatus']; +``` + +The above implementation completely changes the original signature of `res.sendStatus`. It now accepts a status code, encoding type, and the message to be sent to the client. + +In TypeScript, augmenting `express-serve-static-core` adds the new overload to the `Response` type so call sites such as `res.sendStatus(404, 'application/json', body)` type-check. The assignment is cast with `as Response['sendStatus']` because replacing a method with a different signature cannot be checked against the original declaration. + +The overridden method may now be used this way: + +```js +res.sendStatus(404, 'application/json', '{"error":"resource not found"}'); +``` + +## Properties + +Properties in the Express API are either: + +1. Assigned properties (ex: `req.baseUrl`, `req.originalUrl`) +2. Defined as getters (ex: `req.secure`, `req.ip`) + +Since properties under category 1 are dynamically assigned on the `request` and `response` objects in the context of the current request-response cycle, their behavior cannot be overridden. + +Properties under category 2 can be overwritten using the Express API extensions API. + +The following code rewrites how the value of `req.ip` is to be derived. Now, it simply returns the value of the `Client-IP` request header. + +```js +Object.defineProperty(app.request, 'ip', { + configurable: true, + enumerable: true, + get() { + return this.get('Client-IP'); + }, +}); +``` + +```ts +import { type Request } from 'express'; + +Object.defineProperty(app.request, 'ip', { + configurable: true, + enumerable: true, + get(this: Request) { + return this.get('Client-IP'); + }, +}); +``` + +## Extending the API in TypeScript + +The sections above override members that Express already provides. To add your own properties or +methods to the request or response, describe them to TypeScript with +[declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) on the +`Express` namespace. Put the augmentation in a `.d.ts` file that is part of your project. No +`tsconfig.json` change is needed unless a custom `include` does not cover its location. + +For example, an authentication middleware may attach a `user` to the request, and you might add a +`sendError` helper to the response: + +```ts title="types/express.d.ts" +interface User { + id: string; + name: string; +} + +declare global { + namespace Express { + interface Request { + user?: User; + } + interface Response { + sendError(status: number, message: string): this; + } + } +} + +export {}; +``` + +The additions are now known throughout the application: + +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + req.user = { id: '1', name: 'Tobi' }; + next(); +}); + +app.response.sendError = function (this: Response, status: number, message: string) { + return this.status(status).json({ error: message }); +}; + +app.get('/', (req: Request, res: Response) => { + if (!req.user) { + res.sendError(401, 'unauthorized'); + return; + } + res.send(req.user.name); +}); +``` + + + +Declare custom request properties as optional (`user?`). The type applies to every request, but +TypeScript cannot know which middleware ran before a given handler, so a required property would be a +false guarantee. Check for the value (as with `if (!req.user)` above) before relying on it. + + + +Adding a new method needs no cast, unlike [overriding](#methods) an existing method with a different +signature, because the method did not previously exist on the type. + +## Prototype + +In order to provide the Express API, the request/response objects passed to Express (via `app(req, res)`, for example) need to inherit from the same prototype chain. By default, this is `http.IncomingRequest.prototype` for the request and `http.ServerResponse.prototype` for the response. + +Unless necessary, it is recommended that this be done only at the application level, rather than globally. Also, take care that the prototype that is being used matches the functionality as closely as possible to the default prototypes. + +```js +// Use FakeRequest and FakeResponse in place of http.IncomingRequest and http.ServerResponse +// for the given app reference +Object.setPrototypeOf(Object.getPrototypeOf(app.request), FakeRequest.prototype); +Object.setPrototypeOf(Object.getPrototypeOf(app.response), FakeResponse.prototype); +``` diff --git a/src/content/docs/en/5x/guide/routing.mdx b/src/content/docs/en/5x/guide/routing.mdx index 2a0c3813d7..cc05cefe3e 100644 --- a/src/content/docs/en/5x/guide/routing.mdx +++ b/src/content/docs/en/5x/guide/routing.mdx @@ -1,6 +1,6 @@ --- title: Routing -description: Learn how to define and use routes in Express.js applications, including route methods, route paths, parameters, and using Router for modular routing. +description: Learn how to define and use routes in Express.js applications, including route methods, route paths, parameters, typing requests and responses in TypeScript, and using Router for modular routing. --- import Alert from '@components/primitives/Alert/Alert.astro'; @@ -42,6 +42,17 @@ app.get('/', (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +// respond with "hello world" when a GET request is made to the homepage +app.get('/', (req: Request, res: Response) => { + res.send('hello world'); +}); +``` + ## Route methods A route method is derived from one of the HTTP methods, and is attached to an instance of the `express` class. @@ -60,6 +71,20 @@ app.post('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +// GET method route +app.get('/', (req: Request, res: Response) => { + res.send('GET request to the homepage'); +}); + +// POST method route +app.post('/', (req: Request, res: Response) => { + res.send('POST request to the homepage'); +}); +``` + Express supports methods that correspond to all HTTP request methods: `get`, `post`, and so on. For a full list, see [app.METHOD](/api/application#appmethod). @@ -72,6 +97,15 @@ app.all('/secret', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.all('/secret', (req: Request, res: Response, next: NextFunction) => { + console.log('Accessing the secret section ...'); + next(); // pass control to the next handler +}); +``` + ## Route paths Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings or regular expressions. @@ -103,6 +137,22 @@ app.get('/random.text', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.send('root'); +}); + +app.get('/about', (req: Request, res: Response) => { + res.send('about'); +}); + +app.get('/random.text', (req: Request, res: Response) => { + res.send('random.text'); +}); +``` + ### Wildcards Wildcards match any path after a prefix. They must have a name, just like route parameters, and are captured as arrays of path segments. @@ -116,6 +166,17 @@ app.get('/files/*filepath', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/files/*filepath', (req: Request<{ filepath: string[] }>, res: Response) => { + // GET /files/images/logo.png + console.dir(req.params.filepath); + // => [ 'images', 'logo.png' ] + res.send(`File: ${req.params.filepath.join('/')}`); +}); +``` + To also match the root path, wrap the wildcard in braces: ```js @@ -127,6 +188,17 @@ app.get('/{*splat}', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +// Matches / , /foo , /foo/bar , etc. +app.get('/{*splat}', (req: Request, res: Response) => { + // GET / => req.params.splat = [] + // GET /foo/bar => req.params.splat = [ 'foo', 'bar' ] + res.send('ok'); +}); +``` + ### Optional segments Use braces to define optional segments in a route path. When the segment is not present, the parameter is omitted from `req.params`. @@ -139,6 +211,16 @@ app.get('/:file{.:ext}', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/:file{.:ext}', (req: Request, res: Response) => { + // GET /image.png => req.params = { file: 'image', ext: 'png' } + // GET /image => req.params = { file: 'image' } + res.send('ok'); +}); +``` + The characters `?`, `+`, `*`, `[]`, and `()` are reserved and cannot be used as literal characters in route paths. Use `\` to escape them if needed. @@ -161,6 +243,20 @@ app.get(/.*fly$/, (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +// Matches any path containing "a" +app.get(/a/, (req: Request, res: Response) => { + res.send('/a/'); +}); + +// Matches paths ending with "fly" (butterfly, dragonfly, etc.) +app.get(/.*fly$/, (req: Request, res: Response) => { + res.send('/.*fly$/'); +}); +``` + ## Route parameters Route parameters are named URL segments that are used to capture the values specified at their position in the URL. The captured values are populated in the `req.params` object, with the name of the route parameter specified in the path as their respective keys. @@ -179,6 +275,22 @@ app.get('/users/:userId/books/:bookId', (req, res) => { }); ``` +In TypeScript, `@types/express` infers the parameters from the route path, so in the handler above +`req.params.userId` and `req.params.bookId` are already typed as `string` with no extra annotation. +Reading a name that is not in the route (such as `req.params.other`) is a type error. You only need +to annotate the parameters when the handler is defined separately from the route, because the type +checker can no longer see the path. In that case, pass them as the first type argument of `Request`: + +```ts +import { type Request, type Response } from 'express'; + +const sendParams = (req: Request<{ userId: string; bookId: string }>, res: Response) => { + res.send(req.params); +}; + +app.get('/users/:userId/books/:bookId', sendParams); +``` + The name of route parameters must be made up of "word characters" ([A-Za-z0-9_]). @@ -223,6 +335,21 @@ app.get('/user/:id', (req, res) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + if (req.params.id === '0') { + return next('route'); + } + res.send(`User ${req.params.id}`); +}); + +app.get('/user/:id', (req: Request, res: Response) => { + res.send('Special handler for user ID 0'); +}); +``` + In this example: - `GET /user/5` → handled by first route → sends "User 5" @@ -238,6 +365,14 @@ app.get('/example/a', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/example/a', (req: Request, res: Response) => { + res.send('Hello from A!'); +}); +``` + More than one callback function can handle a route (make sure you specify the `next` object). For example: ```js @@ -253,6 +388,21 @@ app.get( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/example/b', + (req: Request, res: Response, next: NextFunction) => { + console.log('the response will be sent by the next function ...'); + next(); + }, + (req: Request, res: Response) => { + res.send('Hello from B!'); + } +); +``` + An array of callback functions can handle a route. For example: ```js @@ -273,6 +423,26 @@ const cb2 = function (req, res) { app.get('/example/c', [cb0, cb1, cb2]); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const cb0 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB0'); + next(); +}; + +const cb1 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB1'); + next(); +}; + +const cb2 = function (req: Request, res: Response) { + res.send('Hello from C!'); +}; + +app.get('/example/c', [cb0, cb1, cb2]); +``` + A combination of independent functions and arrays of functions can handle a route. For example: ```js @@ -299,6 +469,32 @@ app.get( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const cb0 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB0'); + next(); +}; + +const cb1 = function (req: Request, res: Response, next: NextFunction) { + console.log('CB1'); + next(); +}; + +app.get( + '/example/d', + [cb0, cb1], + (req: Request, res: Response, next: NextFunction) => { + console.log('the response will be sent by the next function ...'); + next(); + }, + (req: Request, res: Response) => { + res.send('Hello from D!'); + } +); +``` + ## Response methods The methods on the response object (`res`) in the following table can send a response to the client, and terminate the request-response cycle. If none of these methods are called from a route handler, the client request will be left hanging. @@ -336,6 +532,22 @@ app }); ``` +```ts +import { type Request, type Response } from 'express'; + +app + .route('/book') + .get((req: Request, res: Response) => { + res.send('Get a random book'); + }) + .post((req: Request, res: Response) => { + res.send('Add a book'); + }) + .put((req: Request, res: Response) => { + res.send('Update the book'); + }); +``` + ## express.Router Use the `express.Router` class to create modular, mountable route handlers. A `Router` instance is a complete middleware and routing system; for this reason, it is often referred to as a "mini-app". @@ -391,6 +603,30 @@ router.get('/about', (req, res) => { export default router; ``` +```ts title="birds.ts" +import express, { type Request, type Response, type NextFunction } from 'express'; + +const router = express.Router(); + +// middleware that is specific to this router +const timeLog = (req: Request, res: Response, next: NextFunction) => { + console.log('Time: ', Date.now()); + next(); +}; +router.use(timeLog); + +// define the home page route +router.get('/', (req: Request, res: Response) => { + res.send('Birds home page'); +}); +// define the about route +router.get('/about', (req: Request, res: Response) => { + res.send('About birds'); +}); + +export default router; +``` + Then, load the router module in the app: ```cjs title="index.cjs" diff --git a/src/content/docs/en/5x/guide/using-middleware.mdx b/src/content/docs/en/5x/guide/using-middleware.mdx index 77723e2f37..cd950923ee 100644 --- a/src/content/docs/en/5x/guide/using-middleware.mdx +++ b/src/content/docs/en/5x/guide/using-middleware.mdx @@ -57,6 +57,17 @@ app.use((req, res, next) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +app.use((req: Request, res: Response, next: NextFunction) => { + console.log('Time:', Date.now()); + next(); +}); +``` + This example shows a middleware function mounted on the `/user/:id` path. The function is executed for any type of HTTP request on the `/user/:id` path. @@ -67,6 +78,15 @@ app.use('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use('/user/:id', (req: Request, res: Response, next: NextFunction) => { + console.log('Request Type:', req.method); + next(); +}); +``` + This example shows a route and its handler function (middleware system). The function handles GET requests to the `/user/:id` path. ```js @@ -75,6 +95,14 @@ app.get('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + res.send('USER'); +}); +``` + Here is an example of loading a series of middleware functions at a mount point, with a mount path. It illustrates a middleware sub-stack that prints request info for any type of HTTP request to the `/user/:id` path. @@ -92,6 +120,22 @@ app.use( ); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + console.log('Request URL:', req.originalUrl); + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + console.log('Request Type:', req.method); + next(); + } +); +``` + Route handlers enable you to define multiple routes for a path. The example below defines two routes for GET requests to the `/user/:id` path. The second route will not cause any problems, but it will never get called because the first route ends the request-response cycle. This example shows a middleware sub-stack that handles GET requests to the `/user/:id` path. @@ -114,6 +158,26 @@ app.get('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + console.log('ID:', req.params.id); + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + res.send('User Info'); + } +); + +// handler for the /user/:id path, which prints the user ID +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + res.send(req.params.id); +}); +``` + To skip the rest of the middleware functions from a router middleware stack, call `next('route')` to pass control to the next route. @@ -146,6 +210,29 @@ app.get('/user/:id', (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.get( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + // if the user ID is 0, skip to the next route + if (req.params.id === '0') next('route'); + // otherwise pass the control to the next middleware function in this stack + else next(); + }, + (req: Request, res: Response, next: NextFunction) => { + // send a regular response + res.send('regular'); + } +); + +// handler for the /user/:id path, which sends a special response +app.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + res.send('special'); +}); +``` + Middleware can also be declared in an array for reusability. This example shows an array with a middleware sub-stack that handles GET requests to the `/user/:id` path @@ -167,6 +254,25 @@ app.get('/user/:id', logStuff, (req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +function logOriginalUrl(req: Request, res: Response, next: NextFunction) { + console.log('Request URL:', req.originalUrl); + next(); +} + +function logMethod(req: Request, res: Response, next: NextFunction) { + console.log('Request Type:', req.method); + next(); +} + +const logStuff = [logOriginalUrl, logMethod]; +app.get('/user/:id', logStuff, (req: Request, res: Response, next: NextFunction) => { + res.send('User Info'); +}); +``` + ## Router-level middleware Router-level middleware works in the same way as application-level middleware, except it is bound to an instance of `express.Router()`. @@ -175,6 +281,12 @@ Router-level middleware works in the same way as application-level middleware, e const router = express.Router(); ``` +```ts +import express from 'express'; + +const router = express.Router(); +``` + Load router-level middleware by using the `router.use()` and `router.METHOD()` functions. The following example code replicates the middleware system that is shown above for application-level middleware, by using router-level middleware: @@ -278,6 +390,56 @@ router.get('/user/:id', (req, res, next) => { app.use('/', router); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); +const router = express.Router(); + +// a middleware function with no mount path. This code is executed for every request to the router +router.use((req: Request, res: Response, next: NextFunction) => { + console.log('Time:', Date.now()); + next(); +}); + +// a middleware sub-stack shows request info for any type of HTTP request to the /user/:id path +router.use( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + console.log('Request URL:', req.originalUrl); + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + console.log('Request Type:', req.method); + next(); + } +); + +// a middleware sub-stack that handles GET requests to the /user/:id path +router.get( + '/user/:id', + (req: Request, res: Response, next: NextFunction) => { + // if the user ID is 0, skip to the next router + if (req.params.id === '0') next('route'); + // otherwise pass control to the next middleware function in this stack + else next(); + }, + (req: Request, res: Response, next: NextFunction) => { + // render a regular page + res.render('regular'); + } +); + +// handler for the /user/:id path, which renders a special page +router.get('/user/:id', (req: Request, res: Response, next: NextFunction) => { + console.log(req.params.id); + res.render('special'); +}); + +// mount the router on the app +app.use('/', router); +``` + To skip the rest of the router's middleware functions, call `next('router')` to pass control back out of the router instance. @@ -326,6 +488,28 @@ app.use('/admin', router, (req, res) => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); +const router = express.Router(); + +// predicate the router with a check and bail out when needed +router.use((req: Request, res: Response, next: NextFunction) => { + if (!req.headers['x-auth']) return next('router'); + next(); +}); + +router.get('/user/:id', (req: Request, res: Response) => { + res.send('hello, user!'); +}); + +// use the router and 401 anything falling through +app.use('/admin', router, (req: Request, res: Response) => { + res.sendStatus(401); +}); +``` + ## Error-handling middleware @@ -346,6 +530,15 @@ app.use((err, req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + For details about error-handling middleware, see: [Error handling](/guide/error-handling). ## Built-in middleware @@ -388,4 +581,14 @@ const app = express(); app.use(cookieParser()); ``` +```ts title="index.ts" +import express, { type Express } from 'express'; +import cookieParser from 'cookie-parser'; + +const app: Express = express(); + +// load the cookie-parsing middleware +app.use(cookieParser()); +``` + For a partial list of third-party middleware functions that are commonly used with Express, see: [Third-party middleware](/resources/middleware). diff --git a/src/content/docs/en/5x/guide/using-template-engines.mdx b/src/content/docs/en/5x/guide/using-template-engines.mdx index 18de44b3bf..18284a627d 100644 --- a/src/content/docs/en/5x/guide/using-template-engines.mdx +++ b/src/content/docs/en/5x/guide/using-template-engines.mdx @@ -57,6 +57,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.render('index', { title: 'Hey', message: 'Hello there!' }); +}); +``` + When you make a request to the home page, the `index.pug` file will be rendered as HTML. The view engine cache does not cache the contents of the template's output, only the underlying template itself. The view is still re-rendered with every request even when the cache is on. diff --git a/src/content/docs/en/5x/guide/writing-middleware.mdx b/src/content/docs/en/5x/guide/writing-middleware.mdx index 0b77651d62..cc820fc025 100644 --- a/src/content/docs/en/5x/guide/writing-middleware.mdx +++ b/src/content/docs/en/5x/guide/writing-middleware.mdx @@ -52,6 +52,18 @@ app.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(3000); +``` + ### Middleware function myLogger Here is a simple example of a middleware function called "myLogger". This function just prints @@ -65,6 +77,21 @@ const myLogger = function (req, res, next) { }; ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const myLogger = function (req: Request, res: Response, next: NextFunction) { + console.log('LOGGED'); + next(); +}; +``` + +Here `myLogger` is defined on its own rather than passed directly to `app.use()`, so TypeScript has +no context to infer its parameters and they are annotated explicitly. You can instead type the whole +function as `RequestHandler`, which types `req`, `res`, and `next` for you. When the middleware is +written inline in the `app.use()` call, Express infers those three parameters and no annotations are +needed. + Notice the call above to `next()`. Calling this function invokes the next middleware function in @@ -114,6 +141,25 @@ app.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +const myLogger = function (req: Request, res: Response, next: NextFunction) { + console.log('LOGGED'); + next(); +}; + +app.use(myLogger); + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(3000); +``` + Every time the app receives a request, it prints the message "LOGGED" to the terminal. The order of middleware loading is important: middleware functions that are loaded first are also executed first. @@ -127,6 +173,32 @@ The middleware function `myLogger` simply prints a message, then passes on the r Next, we'll create a middleware function called "requestTime" and add a property called `requestTime` to the request object. + + +Because the middleware adds a property to `req`, extend the request type in TypeScript with +[declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html). Declare +the property (as optional, since a middleware may not run for every request) in a `.d.ts` file that +is part of your project: + +```ts title="types/express.d.ts" +declare global { + namespace Express { + interface Request { + requestTime?: number; + } + } +} + +export {}; +``` + +TypeScript picks up the file automatically, so no `tsconfig.json` change is needed unless you have +set a custom `include` that does not cover its location. See +[Extending the API in TypeScript](/guide/overriding-express-api#extending-the-api-in-typescript) for +adding other custom properties and methods. + + + ```js const requestTime = function (req, res, next) { req.requestTime = Date.now(); @@ -134,6 +206,15 @@ const requestTime = function (req, res, next) { }; ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +const requestTime = function (req: Request, res: Response, next: NextFunction) { + req.requestTime = Date.now(); + next(); +}; +``` + The app now uses the `requestTime` middleware function. Also, the callback function of the root path route uses the property that the middleware function adds to `req` (the request object). ```cjs title="index.cjs" @@ -177,6 +258,27 @@ app.get('/', (req, res) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; + +const app: Express = express(); + +const requestTime = function (req: Request, res: Response, next: NextFunction) { + req.requestTime = Date.now(); + next(); +}; + +app.use(requestTime); + +app.get('/', (req: Request, res: Response) => { + let responseText = 'Hello World!
'; + responseText += `Requested at: ${req.requestTime}`; + res.send(responseText); +}); + +app.listen(3000); +``` + When you make a request to the root of the app, the app now displays the timestamp of your request in the browser. ### Middleware function validateCookies @@ -195,6 +297,16 @@ async function cookieValidator(cookies) { } ``` +```ts +async function cookieValidator(cookies: Record) { + try { + await externallyValidateCookie(cookies.testCookie); + } catch { + throw new Error('Invalid cookies'); + } +} +``` + Here, we use the [`cookie-parser`](/resources/middleware/cookie-parser) middleware to parse incoming cookies off the `req` object and pass them to our `cookieValidator` function. The `validateCookies` middleware returns a Promise that upon rejection will automatically trigger our error handler. ```cjs title="index.cjs" @@ -245,6 +357,30 @@ app.use((err, req, res, next) => { app.listen(3000); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response, type NextFunction } from 'express'; +import cookieParser from 'cookie-parser'; +import cookieValidator from './cookieValidator'; + +const app: Express = express(); + +async function validateCookies(req: Request, res: Response, next: NextFunction) { + await cookieValidator(req.cookies); + next(); +} + +app.use(cookieParser()); + +app.use(validateCookies); + +// error handler +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + res.status(400).send(err.message); +}); + +app.listen(3000); +``` + Note how `next()` is called after `await cookieValidator(req.cookies)`. This ensures that if @@ -281,6 +417,17 @@ export default function (options) { } ``` +```ts title="my-middleware.ts" +import { type Request, type Response, type NextFunction } from 'express'; + +export default function (options: Record) { + return function (req: Request, res: Response, next: NextFunction) { + // Implement the middleware function based on the options object + next(); + }; +} +``` + The middleware can now be used as shown below. ```cjs title="index.cjs" diff --git a/src/content/docs/en/5x/starter/basic-routing.mdx b/src/content/docs/en/5x/starter/basic-routing.mdx index 7751b027a6..2b78e00490 100644 --- a/src/content/docs/en/5x/starter/basic-routing.mdx +++ b/src/content/docs/en/5x/starter/basic-routing.mdx @@ -40,6 +40,14 @@ app.get('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); +``` + Respond to a POST request on the root route (`/`), the application's home page: ```js @@ -48,6 +56,14 @@ app.post('/', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.post('/', (req: Request, res: Response) => { + res.send('Got a POST request'); +}); +``` + Respond to a PUT request to the `/user` route: ```js @@ -56,6 +72,14 @@ app.put('/user', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.put('/user', (req: Request, res: Response) => { + res.send('Got a PUT request at /user'); +}); +``` + Respond to a DELETE request to the `/user` route: ```js @@ -64,4 +88,12 @@ app.delete('/user', (req, res) => { }); ``` +```ts +import { type Request, type Response } from 'express'; + +app.delete('/user', (req: Request, res: Response) => { + res.send('Got a DELETE request at /user'); +}); +``` + For more details about routing, see the [routing guide](/guide/routing). diff --git a/src/content/docs/en/5x/starter/faq.md b/src/content/docs/en/5x/starter/faq.mdx similarity index 89% rename from src/content/docs/en/5x/starter/faq.md rename to src/content/docs/en/5x/starter/faq.mdx index 147a2df632..d60e1d54c6 100755 --- a/src/content/docs/en/5x/starter/faq.md +++ b/src/content/docs/en/5x/starter/faq.mdx @@ -60,6 +60,14 @@ app.use((req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((req: Request, res: Response, next: NextFunction) => { + res.status(404).send("Sorry can't find that!"); +}); +``` + Add routes dynamically at runtime on an instance of `express.Router()` so the routes are not superseded by a middleware function. @@ -75,6 +83,15 @@ app.use((err, req, res, next) => { }); ``` +```ts +import { type Request, type Response, type NextFunction } from 'express'; + +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); +``` + For more information, see [Error handling](/guide/error-handling). ## How do I render plain HTML? diff --git a/src/content/docs/en/5x/starter/hello-world.mdx b/src/content/docs/en/5x/starter/hello-world.mdx index 4c26ad0813..faed0615b2 100644 --- a/src/content/docs/en/5x/starter/hello-world.mdx +++ b/src/content/docs/en/5x/starter/hello-world.mdx @@ -43,6 +43,21 @@ app.listen(port, () => { }); ``` +```ts title="index.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); +const port = 3000; + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); +``` + This app starts a server and listens on port 3000 for connections. The app responds with "Hello World!" for requests to the root URL (`/`) or _route_. For every other path, it will respond with a **404 Not Found**. diff --git a/src/content/docs/en/5x/starter/installing.mdx b/src/content/docs/en/5x/starter/installing.mdx index 30424bae35..27a32d62dd 100755 --- a/src/content/docs/en/5x/starter/installing.mdx +++ b/src/content/docs/en/5x/starter/installing.mdx @@ -1,6 +1,6 @@ --- title: Installing -description: Learn how to install Express.js in your Node.js environment, including setting up your project directory and managing dependencies with npm. +description: Learn how to install Express.js in your Node.js environment, including setting up your project directory, managing dependencies with npm, and configuring TypeScript. --- import Alert from '@components/primitives/Alert/Alert.astro'; @@ -34,3 +34,76 @@ Now, install Express in the `myapp` directory and save it in the dependencies li To install Express temporarily and not add it to the dependencies list: + +## TypeScript + +Express is written in JavaScript and does not bundle its own type definitions. To use it with +TypeScript, install TypeScript together with the community-maintained types for Express and Node.js +(from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)) as development +dependencies: + + + + + +Some middleware does not bundle its own type definitions. If you add an official middleware package +that TypeScript reports as untyped, also install its types from DefinitelyTyped as a dev dependency, +for example `@types/cors` alongside `cors`. + + + +Add a `tsconfig.json`. These options mirror how Node.js runs TypeScript and make the compiler reject +non-erasable syntax (such as `enum`s, namespaces, and parameter properties) that Node cannot strip: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "esnext", + "module": "nodenext", + "rewriteRelativeImportExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true + } +} +``` + +Write your application in TypeScript, annotating the request and response objects: + +```ts title="src/app.ts" +import express, { type Express, type Request, type Response } from 'express'; + +const app: Express = express(); + +app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); +}); + +app.listen(3000); +``` + +You do not need to annotate everything. When you pass a handler directly to a route method or to +`app.use()`, Express infers the types of `req`, `res`, and `next`, and it infers route parameters +from the path, so `req.params.id` is a `string` in `app.get('/users/:id', ...)`. Add explicit types +only where TypeScript has no context to infer from: error-handling middleware, whose +`(err, req, res, next)` signature is not inferred, and handlers you define separately from the route. +In those cases, annotate the parameters or type the whole function as `RequestHandler` or +`ErrorRequestHandler`. + +Run the file directly with Node.js, which strips the TypeScript types and runs the result without a +build step: + +```bash +node src/app.ts +``` + + + +Running `.ts` files directly requires Node.js >= 22.18.0 (or >= 23.6.0 on the v23 line) and +TypeScript >= 5.8. Node strips the types but does not type-check them, so run `npx tsc` to type-check +your project. For more details, see the Node.js guide on +[running TypeScript natively](https://nodejs.org/learn/typescript/run-natively). + + diff --git a/src/content/pages/en/resources/middleware/body-parser.mdx b/src/content/pages/en/resources/middleware/body-parser.mdx index 92bca3827f..bb07d26a2e 100644 --- a/src/content/pages/en/resources/middleware/body-parser.mdx +++ b/src/content/pages/en/resources/middleware/body-parser.mdx @@ -53,6 +53,14 @@ Other body parsers you might be interested in: + + +`body-parser` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/content/pages/en/resources/middleware/compression.mdx b/src/content/pages/en/resources/middleware/compression.mdx index 17719320ff..162d32c2c2 100644 --- a/src/content/pages/en/resources/middleware/compression.mdx +++ b/src/content/pages/en/resources/middleware/compression.mdx @@ -31,6 +31,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`compression` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/content/pages/en/resources/middleware/cookie-parser.mdx b/src/content/pages/en/resources/middleware/cookie-parser.mdx index 0ccd0bb0ba..175cc615cc 100644 --- a/src/content/pages/en/resources/middleware/cookie-parser.mdx +++ b/src/content/pages/en/resources/middleware/cookie-parser.mdx @@ -22,6 +22,14 @@ middleware. + + +`cookie-parser` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/content/pages/en/resources/middleware/cookie-session.mdx b/src/content/pages/en/resources/middleware/cookie-session.mdx index f818929b63..d9db6b0052 100644 --- a/src/content/pages/en/resources/middleware/cookie-session.mdx +++ b/src/content/pages/en/resources/middleware/cookie-session.mdx @@ -47,6 +47,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`cookie-session` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/content/pages/en/resources/middleware/cors.mdx b/src/content/pages/en/resources/middleware/cors.mdx index c70429abf1..ef320adc55 100644 --- a/src/content/pages/en/resources/middleware/cors.mdx +++ b/src/content/pages/en/resources/middleware/cors.mdx @@ -42,6 +42,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`cors` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## Usage ### Simple Usage (Enable _All_ CORS Requests) diff --git a/src/content/pages/en/resources/middleware/errorhandler.mdx b/src/content/pages/en/resources/middleware/errorhandler.mdx index 7d784fd534..47fcf1b5f2 100644 --- a/src/content/pages/en/resources/middleware/errorhandler.mdx +++ b/src/content/pages/en/resources/middleware/errorhandler.mdx @@ -39,6 +39,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`errorhandler` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API {/* eslint-disable no-unused-vars */} diff --git a/src/content/pages/en/resources/middleware/method-override.mdx b/src/content/pages/en/resources/middleware/method-override.mdx index 8f5fd7a482..7795fbac65 100644 --- a/src/content/pages/en/resources/middleware/method-override.mdx +++ b/src/content/pages/en/resources/middleware/method-override.mdx @@ -23,6 +23,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`method-override` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API **NOTE** It is very important that this module is used **before** any module that diff --git a/src/content/pages/en/resources/middleware/morgan.mdx b/src/content/pages/en/resources/middleware/morgan.mdx index d748423aae..3c447a0423 100644 --- a/src/content/pages/en/resources/middleware/morgan.mdx +++ b/src/content/pages/en/resources/middleware/morgan.mdx @@ -25,6 +25,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`morgan` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API {/* eslint-disable no-unused-vars */} diff --git a/src/content/pages/en/resources/middleware/multer.mdx b/src/content/pages/en/resources/middleware/multer.mdx index be616b264d..3f3919b277 100644 --- a/src/content/pages/en/resources/middleware/multer.mdx +++ b/src/content/pages/en/resources/middleware/multer.mdx @@ -39,6 +39,14 @@ This README is also available in other languages: + + +`multer` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## Usage Multer adds a `body` object and a `file` or `files` object to the `request` object. The `body` object contains the values of the text fields of the form, the `file` or `files` object contains the files uploaded via the form. diff --git a/src/content/pages/en/resources/middleware/response-time.mdx b/src/content/pages/en/resources/middleware/response-time.mdx index 4bda25039b..acb134509b 100644 --- a/src/content/pages/en/resources/middleware/response-time.mdx +++ b/src/content/pages/en/resources/middleware/response-time.mdx @@ -28,6 +28,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`response-time` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API {/* eslint-disable no-unused-vars */} diff --git a/src/content/pages/en/resources/middleware/serve-favicon.mdx b/src/content/pages/en/resources/middleware/serve-favicon.mdx index 887862b21a..f04a2fdc4d 100644 --- a/src/content/pages/en/resources/middleware/serve-favicon.mdx +++ b/src/content/pages/en/resources/middleware/serve-favicon.mdx @@ -43,6 +43,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`serve-favicon` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ### favicon(path, options) diff --git a/src/content/pages/en/resources/middleware/serve-index.mdx b/src/content/pages/en/resources/middleware/serve-index.mdx index b49ab49eb3..7b556879de 100644 --- a/src/content/pages/en/resources/middleware/serve-index.mdx +++ b/src/content/pages/en/resources/middleware/serve-index.mdx @@ -23,6 +23,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`serve-index` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/content/pages/en/resources/middleware/serve-static.mdx b/src/content/pages/en/resources/middleware/serve-static.mdx index d9becdd2f5..75d496fd8b 100644 --- a/src/content/pages/en/resources/middleware/serve-static.mdx +++ b/src/content/pages/en/resources/middleware/serve-static.mdx @@ -21,6 +21,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`serve-static` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/content/pages/en/resources/middleware/session.mdx b/src/content/pages/en/resources/middleware/session.mdx index ef4c5f9cad..1384c304b0 100644 --- a/src/content/pages/en/resources/middleware/session.mdx +++ b/src/content/pages/en/resources/middleware/session.mdx @@ -21,6 +21,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`express-session` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/content/pages/en/resources/middleware/timeout.mdx b/src/content/pages/en/resources/middleware/timeout.mdx index 40fa16af93..dc6442ab3a 100644 --- a/src/content/pages/en/resources/middleware/timeout.mdx +++ b/src/content/pages/en/resources/middleware/timeout.mdx @@ -23,6 +23,14 @@ This is a [Node.js](https://nodejs.org/en/) module available through the + + +`connect-timeout` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API **NOTE** This module is not recommend as a "top-level" middleware (i.e. diff --git a/src/content/pages/en/resources/middleware/vhost.mdx b/src/content/pages/en/resources/middleware/vhost.mdx index ecfb9c5daa..e66b2c8478 100644 --- a/src/content/pages/en/resources/middleware/vhost.mdx +++ b/src/content/pages/en/resources/middleware/vhost.mdx @@ -17,6 +17,14 @@ import PackageManagerCommand from '@components/patterns/PackageManagerCommand/Pa + + +`vhost` does not include its own TypeScript type definitions. If you use TypeScript, also install the community-maintained types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) as a development dependency: + + + + + ## API ```js diff --git a/src/plugins/remark-code-tabs.mjs b/src/plugins/remark-code-tabs.mjs index b7c7df5107..fd6e59a740 100644 --- a/src/plugins/remark-code-tabs.mjs +++ b/src/plugins/remark-code-tabs.mjs @@ -13,10 +13,11 @@ * import express from 'express' * ``` * - * …or use `cjs` / `mjs` / `ts` fences. A run that includes `cjs`/`mjs` becomes a - * CommonJS / ESM / TypeScript tab strip (`cjs`/`mjs` are rewritten to `js` for - * highlighting). `ts` only joins such a run — on its own it never groups, so - * unrelated TypeScript snippets stay separate: + * …or use `js` / `cjs` / `mjs` / `ts` fences. A run that includes `cjs`/`mjs` + * becomes a CommonJS / ESM / TypeScript tab strip (`cjs`/`mjs` are rewritten to + * `js` for highlighting). A plain `js` block immediately followed by a `ts` + * block becomes a JavaScript / TypeScript tab strip. On their own, `js` and `ts` + * never group, so unrelated snippets stay separate: * * ```cjs * const express = require('express') @@ -49,13 +50,16 @@ const TAB_RE = /\btab="([^"]+)"/; // group into tabs without an explicit `tab="..."` meta. Each maps to a tab // label plus the language expressive-code should actually highlight as. const LANG_TABS = { + js: { label: 'JavaScript', lang: 'js' }, cjs: { label: 'CommonJS', lang: 'js' }, mjs: { label: 'ESM', lang: 'js' }, ts: { label: 'TypeScript', lang: 'ts' }, }; -// Langs that, on their own, justify forming a tab group. `ts` is a normal -// standalone language too, so it only joins a group led by one of these (or a -// `tab="..."` block) — that way two unrelated `ts` snippets never merge. +// Langs that, on their own, justify forming a tab group. `js` and `ts` are +// normal standalone languages too, so neither triggers a group on its own — +// that way two unrelated `js` (or two `ts`) snippets never merge. A run only +// groups via a `cjs`/`mjs`, a `tab="..."` block, or a `js` + `ts` pair (see +// `hasJsTsPair`). const TRIGGER_LANGS = new Set(['cjs', 'mjs']); const COMPONENT_NAME = 'CodeTabs'; const COMPONENT_SOURCE = '@components/primitives/Tabs/CodeTabs.astro'; @@ -76,6 +80,15 @@ function isTrigger(node) { return node?.type === 'code' && (TAB_RE.test(node.meta || '') || TRIGGER_LANGS.has(node.lang)); } +/** + * A run that pairs a JavaScript block with a TypeScript one (the plain + * `js` + `ts` case). Checked on the original fence langs, before `cjs`/`mjs` + * are rewritten to `js`, so a `cjs`/`mjs` run is never mistaken for this. + */ +function hasJsTsPair(run) { + return run.some((node) => node.lang === 'js') && run.some((node) => node.lang === 'ts'); +} + /** * Returns the tab label and normalizes the node so expressive-code sees a plain * block. The two concerns are handled independently so every other meta token @@ -173,10 +186,10 @@ function groupChildren(children) { j++; } - // Only a run of 2+ blocks with an unambiguous trigger (cjs/mjs or `tab="..."`) - // becomes tabs. A run of only `ts` blocks is left alone so unrelated - // TypeScript snippets never merge. - if (run.length >= 2 && run.some(isTrigger)) { + // Only a run of 2+ blocks with an unambiguous trigger (cjs/mjs or + // `tab="..."`), or a plain `js` + `ts` pair, becomes tabs. A run of only + // `js` (or only `ts`) blocks is left alone so unrelated snippets never merge. + if (run.length >= 2 && (run.some(isTrigger) || hasJsTsPair(run))) { const labels = run.map(consumeTabBlock); children.splice(i, run.length, buildCodeTabs(labels, run)); grouped = true; diff --git a/tests/unit/remark-code-tabs.test.mjs b/tests/unit/remark-code-tabs.test.mjs index 084541fc6c..1792d9a614 100644 --- a/tests/unit/remark-code-tabs.test.mjs +++ b/tests/unit/remark-code-tabs.test.mjs @@ -86,6 +86,34 @@ test('ts joins a run led by a cjs/mjs trigger', () => { assert.deepEqual(panelLangs(tabs), ['js', 'ts']); }); +test('groups a plain js + ts pair into JavaScript/TypeScript tabs', () => { + const tree = run([ + code('js', null, "const e = require('express')"), + code('ts', null, "import e from 'express'"), + ]); + const tabs = findTabs(tree); + assert.ok(tabs, 'a CodeTabs element is created'); + assert.equal(labelsOf(tabs), 'JavaScript,TypeScript'); + assert.deepEqual(panelLangs(tabs), ['js', 'ts']); + assert.ok(hasImport(tree)); +}); + +test('does NOT group two standalone js blocks (js needs a ts to pair with)', () => { + const tree = run([code('js', null, 'const a = 1'), code('js', null, 'const b = 2')]); + assert.equal(findTabs(tree), undefined); + assert.deepEqual( + tree.children.map((n) => n.lang), + ['js', 'js'] + ); +}); + +test('js + ts preserves a title="..." filename on the ts panel', () => { + const tabs = findTabs(run([code('js'), code('ts', 'title="index.ts"')])); + assert.equal(labelsOf(tabs), 'JavaScript,TypeScript'); + assert.deepEqual(panelLangs(tabs), ['js', 'ts']); + assert.equal(tabs.children[1].children[0].meta, 'title="index.ts"'); +}); + test('a lone cjs block is not tabbed but its lang is normalized to js', () => { const tree = run([code('cjs', null, "require('x')")]); assert.equal(findTabs(tree), undefined);