├── .npmrc
├── .gitattributes
├── eslint.config.js
├── .editorconfig
├── .github
├── dependabot.yml
├── workflows
│ └── ci.yml
└── stale.yml
├── types
├── index.d.ts
└── index.test-d.ts
├── index.js
├── LICENSE
├── package.json
├── README.md
├── .gitignore
└── test
└── routes.test.js
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-scripts=true
2 | package-lock=false
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set default behavior to automatically convert line endings
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = require('neostandard')({
4 | ignores: require('neostandard').resolveIgnoresFromGitignore(),
5 | ts: true
6 | })
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 |
12 | # [*.md]
13 | # trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | open-pull-requests-limit: 10
8 |
9 | - package-ecosystem: "npm"
10 | directory: "/"
11 | schedule:
12 | interval: "monthly"
13 | open-pull-requests-limit: 10
14 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { RouteOptions, FastifyPluginCallback } from 'fastify'
4 |
5 | declare module 'fastify' {
6 | interface FastifyInstance {
7 | routes: fastifyRoutes.FastifyRoutes;
8 | }
9 | }
10 |
11 | declare namespace fastifyRoutes {
12 | export type FastifyRoutes = Map
13 |
14 | export const fastifyRoutes: FastifyPluginCallback
15 | export { fastifyRoutes as default }
16 | }
17 |
18 | declare function fastifyRoutes (...params: Parameters): ReturnType
19 | export = fastifyRoutes
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fp = require('fastify-plugin')
4 |
5 | function fastifyRoutes (fastify, _options, next) {
6 | fastify.decorate('routes', new Map())
7 |
8 | fastify.addHook('onRoute', (routeOptions) => {
9 | const { url } = routeOptions
10 |
11 | let routeListForUrl = fastify.routes.get(url)
12 | if (!routeListForUrl) {
13 | routeListForUrl = []
14 | fastify.routes.set(url, routeListForUrl)
15 | }
16 |
17 | routeListForUrl.push(routeOptions)
18 | })
19 |
20 | next()
21 | }
22 |
23 | module.exports = fp(fastifyRoutes, {
24 | fastify: '5.x',
25 | name: '@fastify/routes'
26 | })
27 | module.exports.default = fastifyRoutes
28 | module.exports.fastifyRoutes = fastifyRoutes
29 |
--------------------------------------------------------------------------------
/types/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import fastify, {
2 | FastifyInstance,
3 | FastifyPluginCallback,
4 | RouteOptions,
5 | } from 'fastify'
6 | import { expectAssignable, expectError, expectType } from 'tsd'
7 |
8 | import fastifyRoutes, { FastifyRoutes } from '..'
9 |
10 | const app: FastifyInstance = fastify()
11 | app.register(fastifyRoutes)
12 |
13 | expectType(fastifyRoutes)
14 |
15 | expectError(
16 | app.register(fastifyRoutes, {
17 | unknownOption: 'this should trigger a typescript error',
18 | })
19 | )
20 |
21 | // Plugin property available
22 | app.after(() => {
23 | expectType(app.routes)
24 |
25 | expectAssignable(app.routes.get('/rotue'))
26 | })
27 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - next
8 | - 'v*'
9 | paths-ignore:
10 | - 'docs/**'
11 | - '*.md'
12 | pull_request:
13 | paths-ignore:
14 | - 'docs/**'
15 | - '*.md'
16 |
17 | # This allows a subsequently queued workflow run to interrupt previous runs
18 | concurrency:
19 | group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
20 | cancel-in-progress: true
21 |
22 | permissions:
23 | contents: read
24 |
25 | jobs:
26 | test:
27 | permissions:
28 | contents: write
29 | pull-requests: write
30 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
31 | with:
32 | license-check: true
33 | lint: true
34 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 15
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - "discussion"
8 | - "feature request"
9 | - "bug"
10 | - "help wanted"
11 | - "plugin suggestion"
12 | - "good first issue"
13 | # Label to use when marking an issue as stale
14 | staleLabel: stale
15 | # Comment to post when marking an issue as stale. Set to `false` to disable
16 | markComment: >
17 | This issue has been automatically marked as stale because it has not had
18 | recent activity. It will be closed if no further activity occurs. Thank you
19 | for your contributions.
20 | # Comment to post when closing a stale issue. Set to `false` to disable
21 | closeComment: false
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present Cemre Mengu
4 | Copyright (c) 2018-present The Fastify team
5 |
6 | The Fastify team members are listed at https://github.com/fastify/fastify#team.
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | SOFTWARE.
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@fastify/routes",
3 | "version": "6.0.2",
4 | "description": "A plugin for Fastify that provides a map of routes",
5 | "main": "index.js",
6 | "type": "commonjs",
7 | "types": "types/index.d.ts",
8 | "scripts": {
9 | "lint": "eslint",
10 | "lint:fix": "eslint --fix",
11 | "test": "npm run test:unit && npm run test:typescript",
12 | "test:unit": "c8 --100 node --test",
13 | "test:typescript": "tsd"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+ssh://git@github.com/fastify/fastify-routes.git"
18 | },
19 | "keywords": [
20 | "fastify"
21 | ],
22 | "author": "Cemre Mengu ",
23 | "contributors": [
24 | {
25 | "name": "Matteo Collina",
26 | "email": "hello@matteocollina.com"
27 | },
28 | {
29 | "name": "James Sumners",
30 | "url": "https://james.sumners.info"
31 | },
32 | {
33 | "name": "Aras Abbasi",
34 | "email": "aras.abbasi@gmail.com"
35 | },
36 | {
37 | "name": "Frazer Smith",
38 | "email": "frazer.dev@icloud.com",
39 | "url": "https://github.com/fdawgs"
40 | }
41 | ],
42 | "license": "MIT",
43 | "bugs": {
44 | "url": "https://github.com/fastify/fastify-routes/issues"
45 | },
46 | "homepage": "https://github.com/fastify/fastify-routes#readme",
47 | "funding": [
48 | {
49 | "type": "github",
50 | "url": "https://github.com/sponsors/fastify"
51 | },
52 | {
53 | "type": "opencollective",
54 | "url": "https://opencollective.com/fastify"
55 | }
56 | ],
57 | "devDependencies": {
58 | "@types/node": "^24.0.8",
59 | "c8": "^10.1.2",
60 | "eslint": "^9.17.0",
61 | "fastify": "^5.0.0",
62 | "neostandard": "^0.12.0",
63 | "tsd": "^0.33.0"
64 | },
65 | "dependencies": {
66 | "fastify-plugin": "^5.0.0"
67 | },
68 | "publishConfig": {
69 | "access": "public"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @fastify/routes
2 |
3 | [](https://github.com/fastify/fastify-routes/actions/workflows/ci.yml)
4 | [](https://www.npmjs.com/package/@fastify/routes)
5 | [](https://github.com/neostandard/neostandard)
6 |
7 | This plugin decorates a Fastify instance with `routes`, which is a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) of registered routes. Note that you have to await the registration of this plugin before registering any routes so that @fastify/routes can collect them.
8 |
9 | ## Data Structure
10 |
11 | The `fastify.routes` Map has a key for each path any route has been registered, which points to an array of routes registered on that path. There can be more than one route for a given path if there are multiple routes added with different methods or different constraints.
12 |
13 | ```js
14 | {
15 | '/hello': [
16 | {
17 | method: 'GET',
18 | url: '/hello',
19 | schema: { ... },
20 | handler: Function,
21 | prefix: String,
22 | logLevel: String,
23 | bodyLimit: Number,
24 | constraints: undefined,
25 | },
26 | {
27 | method: 'POST',
28 | url: '/hello',
29 | schema: { ... },
30 | handler: Function,
31 | prefix: String,
32 | logLevel: String,
33 | bodyLimit: Number,
34 | constraints: { ... },
35 | }
36 | ]
37 | }
38 | ```
39 |
40 | ## Example
41 |
42 | ```js
43 | const fastify = require("fastify")();
44 |
45 | (async () => {
46 | await fastify.register(require("@fastify/routes"));
47 | fastify.get("/hello", {}, (request, reply) => {
48 | reply.send({ hello: "world" });
49 | });
50 |
51 | fastify.listen({ port: 3000 }, (err, address) => {
52 | if (err) {
53 | console.error(err);
54 | return;
55 | }
56 | console.log(fastify.routes);
57 | /* will output a Map with entries:
58 | {
59 | '/hello': [
60 | {
61 | method: 'GET',
62 | url: '/hello',
63 | schema: Object,
64 | handler: ,
65 | prefix: ,
66 | logLevel: ,
67 | bodyLimit:
68 | }
69 | ]
70 | }
71 | */
72 | });
73 | })();
74 | ```
75 |
76 | ## License
77 |
78 | MIT License
79 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | # Vim swap files
133 | *.swp
134 |
135 | # macOS files
136 | .DS_Store
137 |
138 | # Clinic
139 | .clinic
140 |
141 | # lock files
142 | bun.lockb
143 | package-lock.json
144 | pnpm-lock.yaml
145 | yarn.lock
146 |
147 | # editor files
148 | .vscode
149 | .idea
150 |
151 | #tap files
152 | .tap/
153 |
--------------------------------------------------------------------------------
/test/routes.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test } = require('node:test')
4 | const Fastify = require('fastify')
5 | const plugin = require('..')
6 |
7 | function handler (_, reply) {
8 | reply.send({ hello: 'world' })
9 | }
10 |
11 | const schema = {
12 | response: {
13 | 200: {
14 | type: 'object',
15 | properties: {
16 | hello: { type: 'string' }
17 | }
18 | }
19 | }
20 | }
21 |
22 | const routeA = function (fastify, _opts, next) {
23 | const options = {
24 | schema,
25 | bodyLimit: 1000,
26 | logLevel: 'warn'
27 | }
28 |
29 | fastify.get('/hello/:world', options, handler)
30 |
31 | next()
32 | }
33 |
34 | const routeB = function (fastify, _opts, next) {
35 | fastify.post(
36 | '/hello/:world',
37 | {
38 | bodyLimit: 2000,
39 | logLevel: 'info'
40 | },
41 | handler
42 | )
43 |
44 | next()
45 | }
46 |
47 | const routeC = {
48 | method: ['GET', 'HEAD'],
49 | path: '/foo',
50 | handler (_req, res) {
51 | res.send({ success: true })
52 | }
53 | }
54 |
55 | const constrainedRoute = {
56 | method: ['GET'],
57 | path: '/foo',
58 | constraints: { host: 'fastify.dev' },
59 | handler (_req, res) {
60 | res.send({ success: true })
61 | }
62 | }
63 |
64 | test('should correctly map routes', async (t) => {
65 | const fastify = Fastify({ exposeHeadRoutes: false })
66 |
67 | await fastify.register(plugin)
68 |
69 | fastify.register(routeA, { prefix: '/v1' })
70 | fastify.register(routeB, { prefix: '/v1' })
71 | fastify.route(routeC)
72 | fastify.route(constrainedRoute)
73 |
74 | await fastify.ready()
75 |
76 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[0].method, 'GET')
77 | t.assert.strictEqual(
78 | fastify.routes.get('/v1/hello/:world')[0].url,
79 | '/v1/hello/:world'
80 | )
81 | t.assert.strictEqual(
82 | fastify.routes.get('/v1/hello/:world')[0].logLevel,
83 | 'warn'
84 | )
85 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[0].prefix, '/v1')
86 | t.assert.strictEqual(
87 | fastify.routes.get('/v1/hello/:world')[0].bodyLimit,
88 | 1000
89 | )
90 | t.assert.strictEqual(
91 | fastify.routes.get('/v1/hello/:world')[0].handler,
92 | handler
93 | )
94 | t.assert.deepStrictEqual(
95 | fastify.routes.get('/v1/hello/:world')[0].schema,
96 | schema
97 | )
98 |
99 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[1].method, 'POST')
100 | t.assert.strictEqual(
101 | fastify.routes.get('/v1/hello/:world')[1].url,
102 | '/v1/hello/:world'
103 | )
104 | t.assert.strictEqual(
105 | fastify.routes.get('/v1/hello/:world')[1].logLevel,
106 | 'info'
107 | )
108 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[1].prefix, '/v1')
109 | t.assert.strictEqual(
110 | fastify.routes.get('/v1/hello/:world')[1].bodyLimit,
111 | 2000
112 | )
113 | t.assert.strictEqual(
114 | fastify.routes.get('/v1/hello/:world')[1].handler,
115 | handler
116 | )
117 |
118 | t.assert.deepStrictEqual(fastify.routes.get('/foo')[0].method, [
119 | 'GET',
120 | 'HEAD'
121 | ])
122 | t.assert.strictEqual(fastify.routes.get('/foo')[0].constraints, undefined)
123 | t.assert.deepStrictEqual(fastify.routes.get('/foo')[1].constraints, {
124 | host: 'fastify.dev'
125 | })
126 | })
127 |
128 | test('should allow other later onRoute handlers to change route options', async (t) => {
129 | const fastify = Fastify({ exposeHeadRoutes: false })
130 |
131 | await fastify.register(plugin)
132 | fastify.addHook('onRoute', (options) => {
133 | options.constraints = { host: 'some-automatic-constraint.com' }
134 | })
135 | fastify.register(routeA)
136 |
137 | await fastify.ready()
138 |
139 | t.assert.strictEqual(
140 | fastify.routes.get('/hello/:world')[0].url,
141 | '/hello/:world'
142 | )
143 | t.assert.deepStrictEqual(fastify.routes.get('/hello/:world')[0].constraints, {
144 | host: 'some-automatic-constraint.com'
145 | })
146 | })
147 |
148 | test('should correctly map routes with automatic HEAD routes', async (t) => {
149 | const fastify = Fastify()
150 |
151 | await fastify.register(plugin)
152 |
153 | fastify.register(routeA, { prefix: '/v1' })
154 | fastify.register(routeB, { prefix: '/v1' })
155 | fastify.route(routeC)
156 | fastify.route(constrainedRoute)
157 |
158 | await fastify.ready()
159 |
160 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[0].method, 'GET')
161 | t.assert.strictEqual(
162 | fastify.routes.get('/v1/hello/:world')[0].url,
163 | '/v1/hello/:world'
164 | )
165 | t.assert.strictEqual(
166 | fastify.routes.get('/v1/hello/:world')[0].logLevel,
167 | 'warn'
168 | )
169 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[0].prefix, '/v1')
170 | t.assert.strictEqual(
171 | fastify.routes.get('/v1/hello/:world')[0].bodyLimit,
172 | 1000
173 | )
174 | t.assert.strictEqual(
175 | fastify.routes.get('/v1/hello/:world')[0].handler,
176 | handler
177 | )
178 | t.assert.deepStrictEqual(
179 | fastify.routes.get('/v1/hello/:world')[0].schema,
180 | schema
181 | )
182 |
183 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[1].method, 'HEAD')
184 | t.assert.strictEqual(
185 | fastify.routes.get('/v1/hello/:world')[1].url,
186 | '/v1/hello/:world'
187 | )
188 | t.assert.strictEqual(
189 | fastify.routes.get('/v1/hello/:world')[1].logLevel,
190 | 'warn'
191 | )
192 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[1].prefix, '/v1')
193 | t.assert.strictEqual(
194 | fastify.routes.get('/v1/hello/:world')[1].bodyLimit,
195 | 1000
196 | )
197 | t.assert.strictEqual(
198 | fastify.routes.get('/v1/hello/:world')[1].handler,
199 | handler
200 | )
201 | t.assert.deepStrictEqual(
202 | fastify.routes.get('/v1/hello/:world')[1].schema,
203 | schema
204 | )
205 |
206 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[2].method, 'POST')
207 | t.assert.strictEqual(
208 | fastify.routes.get('/v1/hello/:world')[2].url,
209 | '/v1/hello/:world'
210 | )
211 | t.assert.strictEqual(
212 | fastify.routes.get('/v1/hello/:world')[2].logLevel,
213 | 'info'
214 | )
215 | t.assert.strictEqual(fastify.routes.get('/v1/hello/:world')[2].prefix, '/v1')
216 | t.assert.strictEqual(
217 | fastify.routes.get('/v1/hello/:world')[2].bodyLimit,
218 | 2000
219 | )
220 | t.assert.strictEqual(
221 | fastify.routes.get('/v1/hello/:world')[2].handler,
222 | handler
223 | )
224 |
225 | t.assert.deepStrictEqual(fastify.routes.get('/foo')[0].method, [
226 | 'GET',
227 | 'HEAD'
228 | ])
229 | t.assert.strictEqual(fastify.routes.get('/foo')[0].constraints, undefined)
230 | t.assert.deepStrictEqual(fastify.routes.get('/foo')[1].constraints, {
231 | host: 'fastify.dev'
232 | })
233 | })
234 |
--------------------------------------------------------------------------------