├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml ├── tests_checker.yml └── workflows │ ├── ci.yml │ ├── package-manager-ci.yml │ └── playwright.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .taprc ├── LICENSE ├── README.md ├── e2e └── custom.spec.js ├── eslint.config.js ├── examples ├── collection-format.js ├── dynamic-openapi.js ├── dynamic-overwrite-endpoint.js ├── dynamic-swagger.js ├── example-e2e.js ├── example-static-specification.js ├── example-static-specification.json ├── example-static-specification.yaml ├── json-in-querystring.js ├── options.js ├── static-json-file.js ├── static-yaml-file.js ├── static │ └── example-logo.svg ├── test-package.json └── theme.js ├── favicon-16x16.png ├── favicon-32x32.png ├── index.js ├── lib ├── index-html.js ├── routes.js ├── serialize.js └── swagger-initializer.js ├── logo.svg ├── package.json ├── playwright.config.js ├── scripts └── prepare-swagger-ui.js ├── test ├── .gitkeep ├── csp.test.js ├── decorator.test.js ├── hooks.test.js ├── integration.test.js ├── prepare.test.js ├── route.test.js ├── serialize.test.js ├── static.test.js ├── swagger-initializer.test.js ├── theme.test.js └── transform-swagger.test.js └── types ├── .gitkeep ├── http2-types.test-d.ts ├── imports.test-d.ts ├── index.d.ts ├── swagger-ui-vendor-extensions.test-d.ts └── types.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set 2 | * text=auto 3 | 4 | # Require Unix line endings 5 | * text eol=lf 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.github/tests_checker.yml: -------------------------------------------------------------------------------- 1 | comment: | 2 | Hello! Thank you for contributing! 3 | It appears that you have changed the code, but the tests that verify your change are missing. Could you please add them? 4 | fileExtensions: 5 | - '.ts' 6 | - '.js' 7 | 8 | testDir: 'test' -------------------------------------------------------------------------------- /.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 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | e2e: 22 | uses: ./.github/workflows/playwright.yml 23 | 24 | test: 25 | permissions: 26 | contents: write 27 | pull-requests: write 28 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 29 | needs: e2e 30 | with: 31 | license-check: true 32 | lint: true 33 | -------------------------------------------------------------------------------- /.github/workflows/package-manager-ci.yml: -------------------------------------------------------------------------------- 1 | name: package-manager-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 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: read 24 | uses: fastify/workflows/.github/workflows/plugins-ci-package-manager.yml@v5 25 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | 16 | timeout-minutes: 60 17 | 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | persist-credentials: false 22 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 23 | with: 24 | check-latest: true 25 | node-version: lts/* 26 | - name: Install dependencies 27 | run: npm i 28 | - name: Install Playwright Browsers 29 | run: npx playwright@1 install chromium --with-deps 30 | - name: Run Playwright tests 31 | run: npx playwright@1 test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # @fastify/swagger-ui specific 2 | /static 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Tap 26 | .tap 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # Snowpack dependency directory (https://snowpack.dev/) 52 | web_modules/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional stylelint cache 64 | .stylelintcache 65 | 66 | # Microbundle cache 67 | .rpt2_cache/ 68 | .rts2_cache_cjs/ 69 | .rts2_cache_es/ 70 | .rts2_cache_umd/ 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | 75 | # Output of 'npm pack' 76 | *.tgz 77 | 78 | # Yarn Integrity file 79 | .yarn-integrity 80 | 81 | # dotenv environment variable files 82 | .env 83 | .env.development.local 84 | .env.test.local 85 | .env.production.local 86 | .env.local 87 | 88 | # parcel-bundler cache (https://parceljs.org/) 89 | .cache 90 | .parcel-cache 91 | 92 | # Next.js build output 93 | .next 94 | out 95 | 96 | # Nuxt.js build / generate output 97 | .nuxt 98 | dist 99 | 100 | # Gatsby files 101 | .cache/ 102 | # Comment in the public line in if your project uses Gatsby and not Next.js 103 | # https://nextjs.org/blog/next-9-1#public-directory-support 104 | # public 105 | 106 | # vuepress build output 107 | .vuepress/dist 108 | 109 | # vuepress v2.x temp and cache directory 110 | .temp 111 | .cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | # Vim swap files 139 | *.swp 140 | 141 | # macOS files 142 | .DS_Store 143 | 144 | # Clinic 145 | .clinic 146 | 147 | # lock files 148 | bun.lockb 149 | package-lock.json 150 | pnpm-lock.yaml 151 | yarn.lock 152 | 153 | # editor files 154 | .vscode 155 | .idea 156 | 157 | #tap files 158 | .tap/ 159 | /test-results/ 160 | /playwright-report/ 161 | /playwright/.cache/ 162 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # @fastify/swagger-ui specific 2 | # /static 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | # Vim swap files 136 | *.swp 137 | 138 | # macOS files 139 | .DS_Store 140 | 141 | # lock files 142 | package-lock.json 143 | pnpm-lock.yaml 144 | yarn.lock 145 | 146 | # editor files 147 | .vscode 148 | .idea -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | files: 2 | - test/**/*.test.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Fastify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @fastify/swagger-ui 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@fastify/swagger-ui.svg?style=flat)](https://www.npmjs.com/package/@fastify/swagger-ui) 4 | [![CI](https://github.com/fastify/fastify-swagger-ui/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-swagger-ui/actions/workflows/ci.yml) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | A Fastify plugin for serving [Swagger UI](https://swagger.io/tools/swagger-ui/). 8 | 9 | ![Demo](https://user-images.githubusercontent.com/52195/228162405-c85ad0d1-900d-442a-b712-7108d98d621f.png) 10 | 11 | 12 | ## Install 13 | ``` 14 | npm i @fastify/swagger-ui 15 | ``` 16 | 17 | ### Compatibility 18 | 19 | | Plugin version | Fastify version | Swagger Plugin Version | 20 | | -------------- | --------------- | ---------------------- | 21 | | `^5.x` | `^5.x` | `^9.x` | 22 | | `^2.x` | `^4.x` | `^8.x` | 23 | | `^1.x` | `^4.x` | `^8.x` | 24 | 25 | 26 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 27 | in the table above. 28 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 29 | 30 | 31 | ## Usage 32 | Add it with `@fastify/swagger` to your project with `register`, pass it some options, call the `swagger` API, and you are done! 33 | 34 | ```js 35 | import fastify from 'fastify' 36 | 37 | const app = fastify() 38 | 39 | await app.register(import('@fastify/swagger')) 40 | 41 | await app.register(import('@fastify/swagger-ui'), { 42 | routePrefix: '/documentation', 43 | uiConfig: { 44 | docExpansion: 'full', 45 | deepLinking: false 46 | }, 47 | uiHooks: { 48 | onRequest: function (request, reply, next) { next() }, 49 | preHandler: function (request, reply, next) { next() } 50 | }, 51 | staticCSP: true, 52 | transformStaticCSP: (header) => header, 53 | transformSpecification: (swaggerObject, request, reply) => { return swaggerObject }, 54 | transformSpecificationClone: true 55 | }) 56 | 57 | app.put('/some-route/:id', { 58 | schema: { 59 | description: 'post some data', 60 | tags: ['user', 'code'], 61 | summary: 'qwerty', 62 | params: { 63 | type: 'object', 64 | properties: { 65 | id: { 66 | type: 'string', 67 | description: 'user id' 68 | } 69 | } 70 | }, 71 | body: { 72 | type: 'object', 73 | properties: { 74 | hello: { type: 'string' }, 75 | obj: { 76 | type: 'object', 77 | properties: { 78 | some: { type: 'string' } 79 | } 80 | } 81 | } 82 | }, 83 | response: { 84 | 201: { 85 | description: 'Successful response', 86 | type: 'object', 87 | properties: { 88 | hello: { type: 'string' } 89 | } 90 | }, 91 | default: { 92 | description: 'Default response', 93 | type: 'object', 94 | properties: { 95 | foo: { type: 'string' } 96 | } 97 | } 98 | }, 99 | security: [ 100 | { 101 | "apiKey": [] 102 | } 103 | ] 104 | } 105 | }, (req, reply) => {}) 106 | 107 | await app.ready() 108 | ``` 109 | 110 | ## API 111 | 112 | 113 | ### Register options 114 | 115 | #### Options 116 | 117 | | Option | Default | Description | 118 | | ------------------ | --------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 119 | | baseDir | undefined | Specify the directory where all spec files that are included in the main one using $ref will be located. By default, this is the directory where the main spec file is located. Provided value should be an absolute path without trailing slash. | 120 | | initOAuth | {} | Configuration options for [Swagger UI initOAuth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). | 121 | | routePrefix | '/documentation' | Overwrite the default Swagger UI route prefix. | 122 | | indexPrefix | '' | Add an additional prefix. This is for when the Fastify server is behind path based routing. ex. NGINX | 123 | | staticCSP | false | Enable CSP header for static resources. | 124 | | transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. | 125 | | transformSpecification | undefined | Synchronous function to transform the swagger document. | 126 | | transformSpecificationClone| true | Provide a deepcloned swaggerObject to transformSpecification | 127 | | uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). | 128 | | uiHooks | {} | Additional hooks for the documentation's routes. You can provide the `onRequest` and `preHandler` hooks with the same [route's options](https://fastify.dev/docs/latest/Reference/Routes/#routes-options) interface. | 129 | | theme | {} | Add custom JavaScript and CSS to the Swagger UI web page | 130 | | logLevel | info | Allow to define route log level. | 131 | 132 | The plugin will expose the documentation with the following APIs: 133 | 134 | | URL | Description | 135 | | ----------------------- | ------------------------------------------ | 136 | | `'/documentation/json'` | The JSON object representing the API | 137 | | `'/documentation/yaml'` | The YAML object representing the API | 138 | | `'/documentation/'` | The swagger UI | 139 | | `'/documentation/*'` | External files that you may use in `$ref` | 140 | 141 | #### uiConfig 142 | 143 | To configure Swagger UI, you need to modify the `uiConfig` option. 144 | It's important to ensure that functions are self-contained. Keep in mind that 145 | you cannot modify the backend code within the `uiConfig` functions, as these 146 | functions are processed only by the browser. You can reference the Swagger UI 147 | element using `ui`, which is assigned to `window.ui`. 148 | 149 | ##### Example 150 | ```js 151 | const fastify = require('fastify')() 152 | 153 | await fastify.register(require('@fastify/swagger')) 154 | 155 | await fastify.register(require('@fastify/swagger-ui'), { 156 | uiConfig: { 157 | onComplete: function () { 158 | alert('ui has type of ' + typeof ui) // 'ui has type of object' 159 | alert('fastify has type of ' + typeof fastify) // 'fastify has type of undefined' 160 | alert('window has type of ' + typeof window) // 'window has type of object' 161 | alert('global has type of ' + typeof global) // 'global has type of undefined' 162 | } 163 | } 164 | }) 165 | ``` 166 | 167 | #### transformSpecification 168 | 169 | There can be use cases, where you want to modify the swagger definition on request. E.g. you want to modify the server 170 | definition based on the hostname of the request object. In such a case you can utilize the transformSpecification-option. 171 | 172 | ##### Example 173 | ```js 174 | const fastify = require('fastify')() 175 | 176 | await fastify.register(require('@fastify/swagger')) 177 | 178 | await fastify.register(require('@fastify/swagger-ui'), { 179 | transformSpecification: (swaggerObject, req, reply) => { 180 | swaggerObject.host = req.hostname 181 | return swaggerObject 182 | } 183 | }) 184 | ``` 185 | 186 | By default fastify.swagger() will be deepcloned and passed to the transformSpecification-function, as fastify.swagger() 187 | returns a mutatable Object. You can disable the deepcloning by setting transformSpecificationClone to false. This is useful, 188 | if you want to handle the deepcloning in the transformSpecification function. 189 | 190 | ##### Example with caching 191 | ```js 192 | const fastify = require('fastify')() 193 | const LRU = require('tiny-lru').lru 194 | const rfdc = require('rfdc')() 195 | 196 | await fastify.register(require('@fastify/swagger')) 197 | 198 | const swaggerLru = new LRU(1000) 199 | await fastify.register(require('@fastify/swagger-ui'), { 200 | transformSpecificationClone: false, 201 | transformSpecification: (swaggerObject, req, reply) => { 202 | if (swaggerLru.has(req.hostname)) { 203 | return swaggerLru.get(req.hostname) 204 | } 205 | const clonedSwaggerObject = rfdc(swaggerObject) 206 | clonedSwaggerObject.host = req.hostname 207 | swaggerLru.set(req.hostname, clonedSwaggerObject) 208 | return clonedSwaggerObject 209 | } 210 | }) 211 | ``` 212 | 213 | #### theme 214 | 215 | You can add custom JavaScript and CSS to the Swagger UI web page by using the theme option. 216 | 217 | ##### Example 218 | 219 | ```js 220 | const fastify = require('fastify')() 221 | 222 | await fastify.register(require('@fastify/swagger')) 223 | 224 | await fastify.register(require('@fastify/swagger-ui'), { 225 | theme: { 226 | title: 'My custom title', 227 | js: [ 228 | { filename: 'special.js', content: 'alert("client javascript")' } 229 | ], 230 | css: [ 231 | { filename: 'theme.css', content: '* { border: 1px red solid; }' } 232 | ], 233 | favicon: [ 234 | { 235 | filename: 'favicon.png', 236 | rel: 'icon', 237 | sizes: '16x16', 238 | type: 'image/png', 239 | content: Buffer.from('iVBOR...', 'base64') 240 | } 241 | ] 242 | } 243 | }) 244 | ``` 245 | 246 | You can add custom JavaScript and CSS to the Swagger UI web page by using the theme option. 247 | 248 | #### logo 249 | 250 | It's possible to override the logo displayed in the top bar by specifying: 251 | 252 | ```js 253 | await fastify.register(require('@fastify/swagger-ui'), { 254 | logo: { 255 | type: 'image/png', 256 | content: Buffer.from('iVBOR...', 'base64'), 257 | href: '/documentation', 258 | target: '_blank' 259 | }, 260 | theme: { 261 | favicon: [ 262 | { 263 | filename: 'favicon.png', 264 | rel: 'icon', 265 | sizes: '16x16', 266 | type: 'image/png', 267 | content: Buffer.from('iVBOR...', 'base64') 268 | } 269 | ] 270 | } 271 | }) 272 | ``` 273 | 274 | #### Protect your documentation routes 275 | 276 | You can protect your documentation by configuring an authentication hook. 277 | Here is an example using the [`@fastify/basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin: 278 | 279 | ##### Example 280 | ```js 281 | const fastify = require('fastify')() 282 | const crypto = require('node:crypto') 283 | 284 | await fastify.register(require('@fastify/swagger')) 285 | 286 | // perform constant-time comparison to prevent timing attacks 287 | function compare (a, b) { 288 | a = Buffer.from(a) 289 | b = Buffer.from(b) 290 | if (a.length !== b.length) { 291 | // Delay return with cryptographically secure timing check. 292 | crypto.timingSafeEqual(a, a) 293 | return false 294 | } 295 | 296 | return crypto.timingSafeEqual(a, b) 297 | } 298 | 299 | await fastify.register(require('@fastify/basic-auth'), { 300 | validate (username, password, req, reply, done) { 301 | let result = true 302 | result = compare(username, validUsername) && result 303 | result = compare(password, validPassword) && result 304 | if (result) { 305 | done() 306 | } else { 307 | done(new Error('Access denied')) 308 | } 309 | }, 310 | authenticate: true 311 | }) 312 | 313 | await fastify.register(require('@fastify/swagger-ui', { 314 | uiHooks: { 315 | onRequest: fastify.basicAuth 316 | } 317 | }) 318 | ``` 319 | 320 | #### Rendering models at the bottom of the page 321 | 322 | To ensure that models are correctly rendered at the bottom of the Swagger UI page, it's important to define your schemas using $refs through [fastify.addSchema](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/#adding-a-shared-schema). Directly embedding JSON schemas within the schema property of your route definitions in Fastify may lead to them not being displayed in Swagger UI. 323 | 324 | #### validatorUrl 325 | 326 | [SwaggerUI](https://github.com/swagger-api/swagger-ui/) can automatically validate the given specification using an online validator. 327 | To enable this behavior you can pass the [`validatorUrl`](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md) option 328 | to this plugin which will be forwarded to SwaggerUI. 329 | 330 | ```js 331 | await fastify.register('@fastify/swagger-ui', { 332 | validatorUrl: 'https://validator.swagger.io/validator' 333 | }) 334 | ``` 335 | 336 | Note that this behavior is disabled by default in `@fastify/swagger-ui`. 337 | 338 | ### Bundling 339 | 340 | To bundle Swagger UI with your application, the swagger-ui static files need to be copied to the server and the `baseDir` option set to point to the file directory. 341 |
342 | Copy files with esbuild 343 | 344 | ```js 345 | import { build } from 'esbuild' 346 | import { copy } from 'esbuild-plugin-copy' 347 | 348 | await build({ 349 | // ... 350 | plugins: [ 351 | copy({ 352 | resolveFrom: 'cwd', 353 | assets: { 354 | from: ['node_modules/@fastify/swagger-ui/static/*'], 355 | to: ['dist/static'], 356 | }, 357 | }), 358 | ], 359 | }) 360 | ``` 361 | 362 |
363 | 364 |
365 | Copy files with docker 366 | 367 | ```Dockerfile 368 | COPY ./node_modules/@fastify/swagger-ui/static /app/static 369 | ``` 370 | 371 |
372 | 373 | #### Configure Swagger UI to use a custom baseDir 374 | Set the `baseDir` option to point to your folder. 375 | 376 | ```js 377 | await fastify.register(require('@fastify/swagger-ui'), { 378 | baseDir: isDev ? undefined : path.resolve('static'), 379 | }) 380 | ``` 381 | 382 | 383 | ## License 384 | 385 | Licensed under [MIT](./LICENSE). 386 | -------------------------------------------------------------------------------- /e2e/custom.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, expect } = require('@playwright/test') 4 | 5 | const URL_DOCUMENTATION = '/documentation' 6 | const URL_FAVICON = '/documentation/static/theme/favicon.svg' 7 | 8 | test.describe('Check customizations', () => { 9 | test('Check JS injection', async ({ page }) => { 10 | await page.goto(URL_DOCUMENTATION) 11 | await page.waitForLoadState('networkidle') 12 | 13 | page.on('dialog', async dialog => { 14 | expect(dialog.type() === 'beforeunload').toBeTruthy() 15 | expect(dialog.message() === 'unloaded test-theme').toBeTruthy() 16 | await dialog.dismiss() 17 | }) 18 | await page.close({ runBeforeUnload: true }) 19 | }) 20 | 21 | test('Check CSS injection', async ({ page }) => { 22 | await page.goto(URL_DOCUMENTATION) 23 | await page.waitForLoadState('networkidle') 24 | 25 | const element = await page.waitForSelector('button.download-url-button') 26 | const color = await element.evaluate(el => window.getComputedStyle(el).getPropertyValue('background-color')) 27 | expect(color).toBe('rgb(255, 0, 0)') 28 | }) 29 | 30 | test('Check custom favicon', async ({ page }) => { 31 | await page.goto(URL_FAVICON) 32 | 33 | const faviconId = await (await page.waitForSelector('svg')).getAttribute('id') 34 | expect(faviconId).toBe('example-logo') // it is included in the svg file 35 | }) 36 | 37 | test('Check custom logo', async ({ page }) => { 38 | await page.goto(URL_DOCUMENTATION) 39 | await page.waitForLoadState('networkidle') 40 | 41 | const logoSrc = await page.locator('img').first().getAttribute('src') 42 | await page.goto(logoSrc) 43 | 44 | const logoId = await (await page.waitForSelector('svg')).getAttribute('id') 45 | expect(logoId).toBe('example-logo') // it is included in the svg file 46 | }) 47 | }) 48 | 49 | test.describe('Check redirection and url handling of static assets', () => { 50 | test('Check static/index.html redirects', async ({ page }) => { 51 | const jsonResponsePromise = page.waitForResponse(/json/) 52 | await page.goto(`${URL_DOCUMENTATION}/static/index.html`) 53 | 54 | // Check if the page is redirected to /documentation 55 | const url = await page.url() 56 | expect(url).toContain(`${URL_DOCUMENTATION}`) 57 | expect(url).not.toContain('static/index.html') 58 | 59 | // Check if the page has requested the json spec, and if so has it succeeded 60 | const jsonResponse = await jsonResponsePromise 61 | expect(jsonResponse.ok()).toBe(true) 62 | }) 63 | 64 | test('Check root UI without slash loads json spec', async ({ page }) => { 65 | const jsonResponsePromise = page.waitForResponse(/json/) 66 | await page.goto(`${URL_DOCUMENTATION}`) 67 | 68 | // Check if the page has requested the json spec, and if so has it succeeded 69 | const jsonResponse = await jsonResponsePromise 70 | expect(jsonResponse.ok()).toBe(true) 71 | }) 72 | 73 | test('Check root UI with trailing slash loads json spec', async ({ page }) => { 74 | const jsonResponsePromise = page.waitForResponse(/json/) 75 | await page.goto(`${URL_DOCUMENTATION}/`) 76 | 77 | // Check if the page has requested the json spec, and if so has it succeeded 78 | const jsonResponse = await jsonResponsePromise 79 | expect(jsonResponse.ok()).toBe(true) 80 | }) 81 | 82 | test('Check root UI with hash loads json spec', async ({ page }) => { 83 | const jsonResponsePromise = page.waitForResponse(/json/) 84 | await page.goto(`${URL_DOCUMENTATION}#default/get_example`) 85 | 86 | // Check if the page has requested the json spec, and if so has it succeeded 87 | const jsonResponse = await jsonResponsePromise 88 | expect(jsonResponse.ok()).toBe(true) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: [ 5 | ...require('neostandard').resolveIgnoresFromGitignore(), 6 | 'dist' 7 | ], 8 | ts: true 9 | }) 10 | -------------------------------------------------------------------------------- /examples/collection-format.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | ; (async () => { 6 | const fastify = Fastify({ 7 | logger: true, 8 | // Need to add a collectionFormat keyword to ajv in fastify instance 9 | ajv: { 10 | customOptions: { 11 | keywords: ['collectionFormat'] 12 | } 13 | } 14 | }) 15 | 16 | await fastify.register(require('@fastify/swagger')) 17 | await fastify.register(require('../index')) 18 | 19 | fastify.route({ 20 | method: 'GET', 21 | url: '/', 22 | schema: { 23 | querystring: { 24 | type: 'object', 25 | required: ['fields'], 26 | additionalProperties: false, 27 | properties: { 28 | fields: { 29 | type: 'array', 30 | items: { 31 | type: 'string' 32 | }, 33 | minItems: 1, 34 | // 35 | // Note that this is an Open API version 2 configuration option. The 36 | // options changed in version 3. The plugin currently only supports 37 | // version 2 of Open API. 38 | // 39 | // Put `collectionFormat` on the same property which you are defining 40 | // as an array of values. (i.e. `collectionFormat` should be a sibling 41 | // of the `type: "array"` specification.) 42 | collectionFormat: 'multi' 43 | } 44 | } 45 | } 46 | }, 47 | handler (request, reply) { 48 | reply.send(request.query.fields) 49 | } 50 | }) 51 | 52 | fastify.listen({ port: 3000 }, (err, addr) => { 53 | if (err) throw err 54 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`) 55 | }) 56 | } 57 | )() 58 | -------------------------------------------------------------------------------- /examples/dynamic-openapi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | ; (async () => { 6 | const fastify = Fastify({ logger: true }) 7 | await fastify.register(require('@fastify/swagger'), { 8 | openapi: { 9 | info: { 10 | title: 'Test swagger', 11 | description: 'testing the fastify swagger api', 12 | version: '0.1.0' 13 | }, 14 | servers: [{ 15 | url: 'http://localhost' 16 | }], 17 | components: { 18 | securitySchemes: { 19 | apiKey: { 20 | type: 'apiKey', 21 | name: 'apiKey', 22 | in: 'header' 23 | } 24 | } 25 | } 26 | }, 27 | hideUntagged: true 28 | }) 29 | 30 | await fastify.register(require('../index'), { 31 | validatorUrl: false 32 | }) 33 | 34 | await fastify.register(async function (fastify) { 35 | fastify.put('/some-route/:id', { 36 | schema: { 37 | description: 'post some data', 38 | tags: ['user', 'code'], 39 | summary: 'qwerty', 40 | security: [{ apiKey: [] }], 41 | params: { 42 | type: 'object', 43 | properties: { 44 | id: { 45 | type: 'string', 46 | description: 'user id' 47 | } 48 | } 49 | }, 50 | body: { 51 | type: 'object', 52 | properties: { 53 | hello: { type: 'string' }, 54 | obj: { 55 | type: 'object', 56 | properties: { 57 | some: { type: 'string' } 58 | } 59 | } 60 | } 61 | }, 62 | response: { 63 | 201: { 64 | description: 'Succesful response', 65 | type: 'object', 66 | properties: { 67 | hello: { type: 'string' } 68 | } 69 | }, 70 | default: { 71 | description: 'Default response', 72 | type: 'object', 73 | properties: { 74 | foo: { type: 'string' } 75 | } 76 | } 77 | } 78 | } 79 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) }) 80 | 81 | fastify.post('/some-route/:id', { 82 | schema: { 83 | description: 'post some data', 84 | summary: 'qwerty', 85 | security: [{ apiKey: [] }], 86 | params: { 87 | type: 'object', 88 | properties: { 89 | id: { 90 | type: 'string', 91 | description: 'user id' 92 | } 93 | } 94 | }, 95 | body: { 96 | type: 'object', 97 | properties: { 98 | hello: { type: 'string' }, 99 | obj: { 100 | type: 'object', 101 | properties: { 102 | some: { type: 'string' } 103 | } 104 | } 105 | } 106 | }, 107 | response: { 108 | 201: { 109 | description: 'Succesful response', 110 | type: 'object', 111 | properties: { 112 | hello: { type: 'string' } 113 | } 114 | } 115 | } 116 | } 117 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) }) 118 | }) 119 | 120 | fastify.listen({ port: 3000, hostname: '0.0.0.0' }, (err, addr) => { 121 | if (err) throw err 122 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`) 123 | }) 124 | })() 125 | -------------------------------------------------------------------------------- /examples/dynamic-overwrite-endpoint.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | ; (async () => { 6 | const fastify = Fastify({ logger: true }) 7 | 8 | await fastify.register(require('@fastify/swagger'), { 9 | swagger: { 10 | info: { 11 | title: 'Test swagger', 12 | description: 'testing the fastify swagger api', 13 | version: '0.1.0' 14 | }, 15 | host: 'localhost', 16 | schemes: ['http'], 17 | consumes: ['application/json'], 18 | produces: ['application/json'] 19 | } 20 | }) 21 | 22 | await fastify.register(require('../index'), { 23 | routePrefix: '/swagger-docs' 24 | }) 25 | 26 | await fastify.register(async function (fastify) { 27 | fastify.put('/some-route/:id', { 28 | schema: { 29 | description: 'post some data', 30 | tags: ['user', 'code'], 31 | summary: 'qwerty', 32 | params: { 33 | type: 'object', 34 | properties: { 35 | id: { 36 | type: 'string', 37 | description: 'user id' 38 | } 39 | } 40 | }, 41 | body: { 42 | type: 'object', 43 | properties: { 44 | hello: { type: 'string' }, 45 | obj: { 46 | type: 'object', 47 | properties: { 48 | some: { type: 'string' } 49 | } 50 | } 51 | } 52 | }, 53 | response: { 54 | 201: { 55 | description: 'Succesful response', 56 | type: 'object', 57 | properties: { 58 | hello: { type: 'string' } 59 | } 60 | } 61 | } 62 | } 63 | }, (req, reply) => {}) 64 | }) 65 | 66 | fastify.listen({ port: 3000, hostname: '0.0.0.0' }, (err, addr) => { 67 | if (err) throw err 68 | fastify.log.info(`Visit the documentation at ${addr}/swagger-docs/`) 69 | }) 70 | })() 71 | -------------------------------------------------------------------------------- /examples/dynamic-swagger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | ; (async () => { 6 | const fastify = Fastify({ logger: true }) 7 | 8 | await fastify.register(require('@fastify/swagger'), { 9 | swagger: { 10 | info: { 11 | title: 'Test swagger', 12 | description: 'testing the fastify swagger api', 13 | version: '0.1.0' 14 | }, 15 | securityDefinitions: { 16 | apiKey: { 17 | type: 'apiKey', 18 | name: 'apiKey', 19 | in: 'header' 20 | } 21 | }, 22 | host: 'localhost:3000', 23 | schemes: ['http'], 24 | consumes: ['application/json'], 25 | produces: ['application/json'] 26 | }, 27 | hideUntagged: true 28 | }) 29 | 30 | await fastify.register(require('../index'), { 31 | routePrefix: '/swagger-docs' 32 | }) 33 | 34 | fastify.addSchema({ 35 | $id: 'user', 36 | type: 'object', 37 | properties: { 38 | id: { 39 | type: 'string', 40 | description: 'user id' 41 | } 42 | } 43 | }) 44 | 45 | fastify.addSchema({ 46 | $id: 'some', 47 | type: 'object', 48 | properties: { 49 | some: { type: 'string' } 50 | } 51 | }) 52 | 53 | await fastify.register(async function (fastify) { 54 | fastify.put('/some-route/:id', { 55 | schema: { 56 | description: 'post some data', 57 | tags: ['user', 'code'], 58 | summary: 'qwerty', 59 | security: [{ apiKey: [] }], 60 | params: { $ref: 'user#' }, 61 | body: { 62 | type: 'object', 63 | properties: { 64 | hello: { type: 'string' }, 65 | obj: { $ref: 'some#' } 66 | } 67 | }, 68 | response: { 69 | 201: { 70 | description: 'Succesful response', 71 | type: 'object', 72 | properties: { 73 | hello: { type: 'string' } 74 | } 75 | }, 76 | default: { 77 | description: 'Default response', 78 | type: 'object', 79 | properties: { 80 | foo: { type: 'string' } 81 | } 82 | } 83 | } 84 | } 85 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) }) 86 | 87 | fastify.post('/some-route/:id', { 88 | schema: { 89 | description: 'post some data', 90 | summary: 'qwerty', 91 | security: [{ apiKey: [] }], 92 | params: { $ref: 'user#' }, 93 | body: { 94 | type: 'object', 95 | properties: { 96 | hello: { type: 'string' }, 97 | obj: { $ref: 'some#' } 98 | } 99 | }, 100 | response: { 101 | 201: { 102 | description: 'Succesful response', 103 | type: 'object', 104 | properties: { 105 | hello: { type: 'string' } 106 | } 107 | } 108 | } 109 | } 110 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) }) 111 | }) 112 | 113 | fastify.listen({ port: 3000, hostname: '0.0.0.0' }, (err, addr) => { 114 | if (err) throw err 115 | fastify.log.info(`Visit the documentation at ${addr}/swagger-docs/`) 116 | }) 117 | })() 118 | -------------------------------------------------------------------------------- /examples/example-e2e.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const readFileSync = require('node:fs').readFileSync 5 | const resolve = require('node:path').resolve 6 | 7 | const exampleLogo = readFileSync( 8 | resolve(__dirname, '..', 'examples/static', 'example-logo.svg'), 9 | 'utf8' 10 | ) 11 | 12 | ; (async () => { 13 | const fastify = Fastify({ logger: true }) 14 | 15 | await fastify.register(require('@fastify/swagger'), { 16 | mode: 'static', 17 | specification: { 18 | path: './examples/example-static-specification.json' 19 | } 20 | }) 21 | 22 | await fastify.register(require('../index'), { 23 | theme: { 24 | js: [ 25 | { filename: 'unloaded.js', content: 'window.onbeforeunload = function(){alert("unloaded test-theme")}' } 26 | ], 27 | css: [ 28 | { filename: 'theme.css', content: '.download-url-button {background: red !important;}' } 29 | ], 30 | favicon: [ 31 | { 32 | filename: 'favicon.svg', 33 | rel: 'icon', 34 | sizes: '16x16', 35 | type: 'image/svg+xml', 36 | content: exampleLogo 37 | } 38 | ] 39 | }, 40 | logo: { 41 | type: 'image/svg+xml', 42 | content: exampleLogo 43 | } 44 | }) 45 | 46 | fastify.listen({ port: process.env.PORT }, (err, addr) => { 47 | if (err) throw err 48 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`) 49 | }) 50 | })() 51 | -------------------------------------------------------------------------------- /examples/example-static-specification.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastify/fastify-swagger-ui/197d6b2dda514b787db3f5628a7f95e4e7f603b5/examples/example-static-specification.js -------------------------------------------------------------------------------- /examples/example-static-specification.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "description": "Test swagger specification", 5 | "version": "1.0.0", 6 | "title": "Test swagger specification", 7 | "contact": { 8 | "email": "super.developer@gmail.com" 9 | } 10 | }, 11 | "servers": [ 12 | { 13 | "url": "http://localhost:3000/", 14 | "description": "Localhost (uses test data)" 15 | } 16 | ], 17 | "paths": { 18 | "/status": { 19 | "get": { 20 | "description": "Status route, so we can check if server is alive", 21 | "tags": [ 22 | "Status" 23 | ], 24 | "responses": { 25 | "200": { 26 | "description": "Server is alive", 27 | "content": { 28 | "application/json": { 29 | "schema": { 30 | "type": "object", 31 | "properties": { 32 | "health": { 33 | "type": "boolean" 34 | }, 35 | "date": { 36 | "type": "string" 37 | } 38 | }, 39 | "example": { 40 | "health": true, 41 | "date": "2018-02-19T15:36:46.758Z" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/example-static-specification.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | description: "Test swagger specification" 4 | version: "1.0.0" 5 | title: "Test swagger specification" 6 | contact: 7 | email: "super.developer@gmail.com" 8 | servers: 9 | - url: http://localhost:3000/ 10 | description: Localhost (uses test data) 11 | paths: 12 | /status: 13 | get: 14 | description: Status route, so we can check if server is alive 15 | tags: 16 | - Status 17 | responses: 18 | 200: 19 | description: 'Server is alive' 20 | content: 21 | application/json: 22 | schema: 23 | type: object 24 | properties: 25 | health: 26 | type: boolean 27 | date: 28 | type: string 29 | example: 30 | health: true 31 | date: "2018-02-19T15:36:46.758Z" -------------------------------------------------------------------------------- /examples/json-in-querystring.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const qs = require('qs') 5 | const Ajv = require('ajv') 6 | 7 | ; (async () => { 8 | const ajv = new Ajv({ 9 | removeAdditional: true, 10 | useDefaults: true, 11 | coerceTypes: true 12 | }) 13 | 14 | const fastify = Fastify({ 15 | logger: true, 16 | querystringParser: (str) => { 17 | const result = qs.parse(str) 18 | 19 | if (result.filter && typeof result.filter === 'string') { 20 | result.filter = JSON.parse(result.filter) 21 | } 22 | 23 | return result 24 | } 25 | }) 26 | 27 | ajv.addKeyword({ 28 | keyword: 'x-consume', 29 | code: (ctx) => Promise.resolve(true) 30 | }) 31 | 32 | fastify.setValidatorCompiler(({ schema }) => ajv.compile(schema)) 33 | 34 | await fastify.register(require('@fastify/swagger'), { 35 | openapi: { 36 | info: { 37 | title: 'Test swagger', 38 | description: 'testing the fastify swagger api', 39 | version: '0.1.0' 40 | } 41 | } 42 | }) 43 | await fastify.register(require('../index')) 44 | 45 | await fastify.register(async function (fastify) { 46 | fastify.route({ 47 | method: 'GET', 48 | url: '/', 49 | schema: { 50 | querystring: { 51 | type: 'object', 52 | required: ['filter'], 53 | additionalProperties: false, 54 | properties: { 55 | filter: { 56 | type: 'object', 57 | required: ['foo'], 58 | properties: { 59 | foo: { type: 'string' }, 60 | bar: { type: 'string' } 61 | }, 62 | 'x-consume': 'application/json' 63 | } 64 | } 65 | } 66 | }, 67 | handler (request, reply) { 68 | reply.send(request.query.filter) 69 | } 70 | }) 71 | }) 72 | 73 | fastify.listen({ port: 3000 }, (err, addr) => { 74 | if (err) throw err 75 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`) 76 | }) 77 | })() 78 | -------------------------------------------------------------------------------- /examples/options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const swaggerOption = { 4 | swagger: { 5 | info: { 6 | title: 'Test swagger', 7 | description: 'testing the fastify swagger api', 8 | version: '0.1.0' 9 | }, 10 | host: 'localhost', 11 | schemes: ['http'], 12 | consumes: ['application/json'], 13 | produces: ['application/json'], 14 | tags: [ 15 | { name: 'tag' } 16 | ], 17 | externalDocs: { 18 | description: 'Find more info here', 19 | url: 'https://swagger.io' 20 | }, 21 | securityDefinitions: { 22 | apiKey: { 23 | type: 'apiKey', 24 | name: 'apiKey', 25 | in: 'header' 26 | } 27 | }, 28 | security: [{ 29 | apiKey: [] 30 | }] 31 | } 32 | } 33 | 34 | const openapiOption = { 35 | openapi: { 36 | info: { 37 | title: 'Test swagger', 38 | description: 'testing the fastify swagger api', 39 | version: '0.1.0' 40 | }, 41 | servers: [ 42 | { 43 | url: 'http://localhost' 44 | } 45 | ], 46 | tags: [ 47 | { name: 'tag' } 48 | ], 49 | components: { 50 | securitySchemes: { 51 | apiKey: { 52 | type: 'apiKey', 53 | name: 'apiKey', 54 | in: 'header' 55 | } 56 | } 57 | }, 58 | security: [{ 59 | apiKey: [] 60 | }], 61 | externalDocs: { 62 | description: 'Find more info here', 63 | url: 'https://swagger.io' 64 | } 65 | } 66 | } 67 | 68 | const openapiRelativeOptions = { 69 | openapi: { 70 | info: { 71 | title: 'Test swagger', 72 | description: 'testing the fastify swagger api', 73 | version: '0.1.0' 74 | }, 75 | servers: [ 76 | { 77 | url: '/test' 78 | } 79 | ], 80 | tags: [ 81 | { name: 'tag' } 82 | ], 83 | components: { 84 | securitySchemes: { 85 | apiKey: { 86 | type: 'apiKey', 87 | name: 'apiKey', 88 | in: 'header' 89 | } 90 | } 91 | }, 92 | security: [{ 93 | apiKey: [] 94 | }], 95 | externalDocs: { 96 | description: 'Find more info here', 97 | url: 'https://swagger.io' 98 | } 99 | }, 100 | stripBasePath: false 101 | } 102 | 103 | const schemaQuerystring = { 104 | schema: { 105 | response: { 106 | 200: { 107 | type: 'object', 108 | properties: { 109 | hello: { type: 'string' } 110 | } 111 | } 112 | }, 113 | querystring: { 114 | type: 'object', 115 | properties: { 116 | hello: { type: 'string' }, 117 | world: { type: 'string' } 118 | } 119 | } 120 | } 121 | } 122 | 123 | const schemaBody = { 124 | schema: { 125 | body: { 126 | type: 'object', 127 | properties: { 128 | hello: { type: 'string' }, 129 | obj: { 130 | type: 'object', 131 | properties: { 132 | some: { type: 'string' }, 133 | constantProp: { const: 'my-const' } 134 | } 135 | } 136 | }, 137 | required: ['hello'] 138 | } 139 | } 140 | } 141 | 142 | const schemaParams = { 143 | schema: { 144 | params: { 145 | type: 'object', 146 | properties: { 147 | id: { 148 | type: 'string', 149 | description: 'user id' 150 | } 151 | } 152 | } 153 | } 154 | } 155 | 156 | const schemaHeaders = { 157 | schema: { 158 | headers: { 159 | type: 'object', 160 | properties: { 161 | authorization: { 162 | type: 'string', 163 | description: 'api token' 164 | } 165 | }, 166 | required: ['authorization'] 167 | } 168 | } 169 | } 170 | 171 | const schemaHeadersParams = { 172 | schema: { 173 | headers: { 174 | type: 'object', 175 | properties: { 176 | 'x-api-token': { 177 | type: 'string', 178 | description: 'optional api token' 179 | }, 180 | 'x-api-version': { 181 | type: 'string', 182 | description: 'optional api version' 183 | } 184 | } 185 | }, 186 | params: { 187 | type: 'object', 188 | properties: { 189 | id: { 190 | type: 'string', 191 | description: 'user id' 192 | } 193 | } 194 | } 195 | } 196 | } 197 | 198 | const schemaSecurity = { 199 | schema: { 200 | security: [ 201 | { 202 | apiKey: [] 203 | } 204 | ] 205 | } 206 | } 207 | 208 | const schemaConsumes = { 209 | schema: { 210 | consumes: ['application/x-www-form-urlencoded'], 211 | body: { 212 | type: 'object', 213 | properties: { 214 | hello: { 215 | description: 'hello', 216 | type: 'string' 217 | } 218 | }, 219 | required: ['hello'] 220 | } 221 | } 222 | } 223 | 224 | const schemaProduces = { 225 | schema: { 226 | produces: ['*/*'], 227 | response: { 228 | 200: { 229 | type: 'object', 230 | properties: { 231 | hello: { 232 | description: 'hello', 233 | type: 'string' 234 | } 235 | }, 236 | required: ['hello'] 237 | } 238 | } 239 | } 240 | } 241 | 242 | const schemaCookies = { 243 | schema: { 244 | cookies: { 245 | type: 'object', 246 | properties: { 247 | bar: { type: 'string' } 248 | } 249 | } 250 | } 251 | } 252 | 253 | const schemaAllOf = { 254 | schema: { 255 | querystring: { 256 | allOf: [ 257 | { 258 | type: 'object', 259 | properties: { 260 | foo: { type: 'string' } 261 | } 262 | } 263 | ] 264 | } 265 | } 266 | } 267 | 268 | const schemaExtension = { 269 | schema: { 270 | 'x-tension': true 271 | } 272 | } 273 | 274 | const schemaOperationId = { 275 | schema: { 276 | operationId: 'helloWorld', 277 | response: { 278 | 200: { 279 | type: 'object', 280 | properties: { 281 | hello: { 282 | description: 'hello', 283 | type: 'string' 284 | } 285 | }, 286 | required: ['hello'] 287 | } 288 | } 289 | } 290 | } 291 | 292 | module.exports = { 293 | openapiOption, 294 | openapiRelativeOptions, 295 | swaggerOption, 296 | schemaQuerystring, 297 | schemaBody, 298 | schemaParams, 299 | schemaHeaders, 300 | schemaHeadersParams, 301 | schemaSecurity, 302 | schemaConsumes, 303 | schemaProduces, 304 | schemaCookies, 305 | schemaAllOf, 306 | schemaExtension, 307 | schemaOperationId 308 | } 309 | -------------------------------------------------------------------------------- /examples/static-json-file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | ; (async () => { 6 | const fastify = Fastify({ logger: true }) 7 | 8 | await fastify.register(require('@fastify/swagger'), { 9 | mode: 'static', 10 | specification: { 11 | path: './examples/example-static-specification.json' 12 | } 13 | }) 14 | 15 | await fastify.register(require('../index')) 16 | 17 | fastify.listen({ port: 3000 }, (err, addr) => { 18 | if (err) throw err 19 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`) 20 | }) 21 | })() 22 | -------------------------------------------------------------------------------- /examples/static-yaml-file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | ; (async () => { 6 | const fastify = Fastify({ logger: true }) 7 | 8 | await fastify.register(require('@fastify/swagger'), { 9 | mode: 'static', 10 | specification: { 11 | path: './examples/example-static-specification.yaml' 12 | } 13 | }) 14 | 15 | await fastify.register(require('../index')) 16 | 17 | fastify.listen({ port: 3000 }, (err, addr) => { 18 | if (err) throw err 19 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`) 20 | }) 21 | })() 22 | -------------------------------------------------------------------------------- /examples/static/example-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/test-package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/theme.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | ; (async () => { 6 | const fastify = Fastify({ logger: true }) 7 | 8 | await fastify.register(require('@fastify/swagger'), { 9 | mode: 'static', 10 | specification: { 11 | path: './examples/example-static-specification.json' 12 | } 13 | }) 14 | await fastify.register(require('../index'), { 15 | theme: { 16 | js: [ 17 | { filename: 'special.js', content: 'alert("loaded test-theme")' } 18 | ], 19 | css: [ 20 | { filename: 'theme.css', content: '* {border: 1px red solid;}' } 21 | ], 22 | favicon: [ 23 | { 24 | filename: 'favicon.png', 25 | rel: 'icon', 26 | sizes: '16x16', 27 | type: 'image/png', 28 | content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAA8AAAAQCAQAAABjX+2PAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAACYktHRAD/h4/MvwAAAAd0SU1FB+cCEQ06N8A8CiUAAADnSURBVBjTrdE/K8QBAMbxz/0TLrnUWcTg7ySLewGEwWDRzSYpyULJbGG6wWBTlMEbkHsFNnVloAwXudIlnDru1O9nOCex3rM89TzL0/eh1Ypo//Zk5CdM6JP2IWFOxbmMKZVmPWzbrJSamG5FNXUFx42yV16oqCQUerNr2pghsSgS1sw4kxNVVvbu3rwjSwJ67Kgq2XMjtO/AnWsnVgwQNy6rQ8GkURWBpCebXnR5gA11j5b1OxT4EKq6dGurMWvQqqw2LPoUKDq1LqPzN4q0rCuvckbE/pOakHdhQfwvwKan8Nzad74AkR8/Ir6qAvAAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjMtMDItMTdUMTM6NTg6NTUrMDA6MDBjkr64AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIzLTAyLTE3VDEzOjU4OjU1KzAwOjAwEs8GBAAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyMy0wMi0xN1QxMzo1ODo1NSswMDowMEXaJ9sAAAAASUVORK5CYII=', 'base64') 29 | } 30 | ] 31 | } 32 | }) 33 | 34 | fastify.listen({ port: 3000 }, (err, addr) => { 35 | if (err) throw err 36 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`) 37 | }) 38 | })() 39 | -------------------------------------------------------------------------------- /favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastify/fastify-swagger-ui/197d6b2dda514b787db3f5628a7f95e4e7f603b5/favicon-16x16.png -------------------------------------------------------------------------------- /favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastify/fastify-swagger-ui/197d6b2dda514b787db3f5628a7f95e4e7f603b5/favicon-32x32.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fsPromises = require('node:fs/promises') 4 | const path = require('node:path') 5 | const fp = require('fastify-plugin') 6 | const csp = require('./static/csp.json') 7 | 8 | async function fastifySwaggerUi (fastify, opts) { 9 | fastify.decorate('swaggerCSP', csp) 10 | 11 | // if no logo is provided, read default static logo 12 | let logoContent = opts.logo 13 | if (logoContent === undefined) { 14 | const bufferLogoContent = await fsPromises.readFile(path.join(__dirname, './static/logo.svg')) 15 | logoContent = { type: 'image/svg+xml', content: bufferLogoContent } 16 | } 17 | 18 | await fastify.register(require('./lib/routes'), { 19 | ...opts, 20 | prefix: opts.routePrefix || '/documentation', 21 | uiConfig: opts.uiConfig || {}, 22 | initOAuth: opts.initOAuth || {}, 23 | hooks: opts.uiHooks, 24 | theme: opts.theme || {}, 25 | logo: logoContent 26 | }) 27 | } 28 | 29 | module.exports = fp(fastifySwaggerUi, { 30 | fastify: '5.x', 31 | name: '@fastify/swagger-ui', 32 | dependencies: ['@fastify/swagger'] 33 | }) 34 | module.exports.default = fastifySwaggerUi 35 | module.exports.fastifySwaggerUi = fastifySwaggerUi 36 | -------------------------------------------------------------------------------- /lib/index-html.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function indexHtml (opts) { 4 | let routePrefix = opts.prefix 5 | if (opts.indexPrefix) { 6 | routePrefix = `${opts.indexPrefix.replace(/\/$/, '')}/${opts.prefix.replace(/^\//, '')}` 7 | } 8 | return (url) => { 9 | const hasTrailingSlash = /\/$/.test(url) 10 | const prefix = hasTrailingSlash ? `.${opts.staticPrefix}` : `${routePrefix}${opts.staticPrefix}` 11 | return ` 12 | 13 | 14 | 15 | 16 | ${opts.theme?.title || 'Swagger UI'} 17 | 18 | 19 | ${opts.theme && opts.theme.css ? opts.theme.css.map(css => `\n`).join('') : ''} 20 | ${opts.theme && opts.theme.favicon 21 | ? opts.theme.favicon.map(favicon => `\n`).join('') 22 | : ` 23 | 24 | 25 | `} 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | ${opts.theme && opts.theme.js ? opts.theme.js.map(js => `\n`).join('') : ''} 34 | 35 | 36 | ` 37 | } 38 | } 39 | 40 | module.exports = indexHtml 41 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('node:path') 4 | const yaml = require('yaml') 5 | const fastifyStatic = require('@fastify/static') 6 | const rfdc = require('rfdc')() 7 | const swaggerInitializer = require('./swagger-initializer') 8 | const indexHtml = require('./index-html') 9 | 10 | // URI prefix to separate static assets for swagger UI 11 | const staticPrefix = '/static' 12 | 13 | function fastifySwagger (fastify, opts, done) { 14 | let staticCSP = false 15 | if (opts.staticCSP === true) { 16 | const csp = fastify.swaggerCSP 17 | staticCSP = `default-src 'self'; base-uri 'self'; font-src 'self' https: data:; frame-ancestors 'self'; img-src 'self' data: validator.swagger.io; object-src 'none'; script-src 'self' ${csp.script.join(' ')}; script-src-attr 'none'; style-src 'self' https: ${csp.style.join(' ')}; upgrade-insecure-requests;` 18 | } 19 | if (typeof opts.staticCSP === 'string') { 20 | staticCSP = opts.staticCSP 21 | } 22 | if (typeof opts.staticCSP === 'object' && opts.staticCSP !== null) { 23 | staticCSP = '' 24 | Object.keys(opts.staticCSP).forEach(function (key) { 25 | const value = Array.isArray(opts.staticCSP[key]) ? opts.staticCSP[key].join(' ') : opts.staticCSP[key] 26 | staticCSP += `${key.toLowerCase()} ${value}; ` 27 | }) 28 | } 29 | 30 | if (typeof staticCSP === 'string' || typeof opts.transformStaticCSP === 'function') { 31 | fastify.addHook('onSend', function (_request, reply, _payload, done) { 32 | // set static csp when it is passed 33 | if (typeof staticCSP === 'string') { 34 | reply.header('content-security-policy', staticCSP.trim()) 35 | } 36 | // mutate the header when it is passed 37 | const header = reply.getHeader('content-security-policy') 38 | if (header && typeof opts.transformStaticCSP === 'function') { 39 | reply.header('content-security-policy', opts.transformStaticCSP(header)) 40 | } 41 | done() 42 | }) 43 | } 44 | 45 | const hooks = Object.create(null) 46 | if (opts.hooks) { 47 | const additionalHooks = [ 48 | 'onRequest', 49 | 'preHandler' 50 | ] 51 | for (const hook of additionalHooks) { 52 | hooks[hook] = opts.hooks[hook] 53 | } 54 | } 55 | 56 | if (opts.theme) { 57 | const themePrefix = `${staticPrefix}/theme` 58 | if (opts.theme.css) { 59 | for (const cssFile of opts.theme.css) { 60 | fastify.route({ 61 | url: `${themePrefix}/${cssFile.filename}`, 62 | method: 'GET', 63 | schema: { hide: true }, 64 | ...hooks, 65 | handler: (_req, reply) => { 66 | reply 67 | .header('content-type', 'text/css; charset=UTF-8') 68 | .send(cssFile.content) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | if (opts.theme.js) { 75 | for (const jsFile of opts.theme.js) { 76 | fastify.route({ 77 | url: `${themePrefix}/${jsFile.filename}`, 78 | method: 'GET', 79 | schema: { hide: true }, 80 | ...hooks, 81 | handler: (_req, reply) => { 82 | reply 83 | .header('content-type', 'application/javascript; charset=utf-8') 84 | .send(jsFile.content) 85 | } 86 | }) 87 | } 88 | } 89 | 90 | if (opts.theme.favicon) { 91 | for (const favicon of opts.theme.favicon) { 92 | fastify.route({ 93 | url: `${themePrefix}/${favicon.filename}`, 94 | method: 'GET', 95 | schema: { hide: true }, 96 | ...hooks, 97 | handler: (_req, reply) => { 98 | reply 99 | .header('content-type', favicon.type) 100 | .send(favicon.content) 101 | } 102 | }) 103 | } 104 | } 105 | } 106 | 107 | const indexHtmlContent = indexHtml({ ...opts, staticPrefix }) 108 | 109 | fastify.route({ 110 | url: '/', 111 | method: 'GET', 112 | schema: { hide: true }, 113 | ...hooks, 114 | handler: (req, reply) => { 115 | reply 116 | .header('content-type', 'text/html; charset=utf-8') 117 | .send(indexHtmlContent(req.url)) // trailing slash alters the relative urls generated in the html 118 | } 119 | }) 120 | 121 | fastify.route({ 122 | url: `${staticPrefix}/index.html`, 123 | method: 'GET', 124 | schema: { hide: true }, 125 | ...hooks, 126 | handler: (req, reply) => { 127 | reply.redirect(req.url.replace(/\/static\/index\.html$/, '/')) 128 | } 129 | }) 130 | 131 | const swaggerInitializerContent = swaggerInitializer(opts) 132 | 133 | fastify.route({ 134 | url: `${staticPrefix}/swagger-initializer.js`, 135 | method: 'GET', 136 | schema: { hide: true }, 137 | ...hooks, 138 | handler: (_req, reply) => { 139 | reply 140 | .header('content-type', 'application/javascript; charset=utf-8') 141 | .send(swaggerInitializerContent) 142 | } 143 | }) 144 | 145 | const hasTransformSpecificationFn = typeof opts.transformSpecification === 'function' 146 | const shouldCloneSwaggerObject = opts.transformSpecificationClone ?? true 147 | const transformSpecification = opts.transformSpecification 148 | fastify.route({ 149 | url: '/json', 150 | method: 'GET', 151 | schema: { hide: true }, 152 | ...hooks, 153 | handler: hasTransformSpecificationFn 154 | ? shouldCloneSwaggerObject 155 | ? function (req, reply) { 156 | reply.send(transformSpecification(rfdc(fastify.swagger()), req, reply)) 157 | } 158 | : function (req, reply) { 159 | reply.send(transformSpecification(fastify.swagger(), req, reply)) 160 | } 161 | : function (_req, reply) { 162 | reply.send(fastify.swagger()) 163 | } 164 | }) 165 | 166 | fastify.route({ 167 | url: '/yaml', 168 | method: 'GET', 169 | schema: { hide: true }, 170 | ...hooks, 171 | handler: hasTransformSpecificationFn 172 | ? shouldCloneSwaggerObject 173 | ? function (req, reply) { 174 | reply 175 | .type('application/x-yaml') 176 | .send(yaml.stringify(transformSpecification(rfdc(fastify.swagger()), req, reply))) 177 | } 178 | : function (req, reply) { 179 | reply 180 | .type('application/x-yaml') 181 | .send(yaml.stringify(transformSpecification(fastify.swagger(), req, reply))) 182 | } 183 | : function (_req, reply) { 184 | reply 185 | .type('application/x-yaml') 186 | .send(fastify.swagger({ yaml: true })) 187 | } 188 | }) 189 | 190 | // serve swagger-ui with the help of @fastify/static 191 | fastify.register(fastifyStatic, { 192 | root: opts.baseDir || path.join(__dirname, '..', 'static'), 193 | prefix: staticPrefix, 194 | decorateReply: false 195 | }) 196 | 197 | if (opts.baseDir) { 198 | fastify.register(fastifyStatic, { 199 | root: opts.baseDir, 200 | serve: false 201 | }) 202 | 203 | // Handler for external documentation files passed via $ref 204 | fastify.route({ 205 | url: '/*', 206 | method: 'GET', 207 | schema: { hide: true }, 208 | ...hooks, 209 | handler: function (req, reply) { 210 | const file = req.params['*'] 211 | reply.sendFile(file) 212 | } 213 | }) 214 | } 215 | 216 | done() 217 | } 218 | 219 | module.exports = fastifySwagger 220 | -------------------------------------------------------------------------------- /lib/serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function serialize (value) { 4 | switch (typeof value) { 5 | case 'bigint': 6 | return value.toString() + 'n' 7 | case 'boolean': 8 | return value ? 'true' : 'false' 9 | case 'function': 10 | return value.toString() 11 | case 'number': 12 | return '' + value 13 | case 'object': 14 | if (value === null) { 15 | return 'null' 16 | } else if (Array.isArray(value)) { 17 | return serializeArray(value) 18 | } else if (value instanceof RegExp) { 19 | return `/${value.source}/${value.flags}` 20 | } else if (value instanceof Date) { 21 | return `new Date(${value.getTime()})` 22 | } else if (value instanceof Set) { 23 | return `new Set(${serializeArray(Array.from(value))})` 24 | } else if (value instanceof Map) { 25 | return `new Map(${serializeArray(Array.from(value))})` 26 | } else { 27 | return serializeObject(value) 28 | } 29 | case 'string': 30 | return JSON.stringify(value) 31 | case 'symbol': 32 | return serializeSymbol(value) 33 | case 'undefined': 34 | return 'undefined' 35 | } 36 | } 37 | const symbolRE = /Symbol\((.+)\)/ 38 | function serializeSymbol (value) { 39 | return symbolRE.test(value.toString()) 40 | ? `Symbol("${value.toString().match(symbolRE)[1]}")` 41 | : 'Symbol()' 42 | } 43 | 44 | function serializeArray (value) { 45 | let result = '[' 46 | const il = value.length 47 | const last = il - 1 48 | for (let i = 0; i < il; ++i) { 49 | result += serialize(value[i]) 50 | i !== last && (result += ',') 51 | } 52 | return result + ']' 53 | } 54 | 55 | function serializeObject (value) { 56 | let result = '{' 57 | const keys = Object.keys(value) 58 | let i = 0 59 | const il = keys.length 60 | const last = il - 1 61 | for (; i < il; ++i) { 62 | const key = keys[i] 63 | result += `"${key}":${serialize(value[key])}` 64 | i !== last && (result += ',') 65 | } 66 | return result + '}' 67 | } 68 | 69 | module.exports = serialize 70 | -------------------------------------------------------------------------------- /lib/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const serialize = require('./serialize') 4 | 5 | function swaggerInitializer (opts) { 6 | const hasLogo = opts.logo && opts.logo.content !== undefined 7 | const logoBase64 = hasLogo && Buffer.from(opts.logo.content).toString('base64') 8 | const logoData = hasLogo && `data:${opts.logo.type};base64,${logoBase64}` 9 | const logoHref = hasLogo && opts.logo.href 10 | const logoTarget = hasLogo && opts.logo.target 11 | 12 | return `window.onload = function () { 13 | function waitForElement(selector) { 14 | return new Promise(resolve => { 15 | if (document.querySelector(selector)) { 16 | return resolve(document.querySelector(selector)); 17 | } 18 | 19 | const observer = new MutationObserver(mutations => { 20 | if (document.querySelector(selector)) { 21 | observer.disconnect(); 22 | resolve(document.querySelector(selector)); 23 | } 24 | }); 25 | 26 | // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336 27 | observer.observe(document.body, { 28 | childList: true, 29 | subtree: true 30 | }); 31 | }); 32 | } 33 | function resolveUrl(url) { 34 | let currentHref = window.location.href; 35 | currentHref = currentHref.split('#', 1)[0]; 36 | currentHref = currentHref.endsWith('/') ? currentHref : currentHref + '/'; 37 | const anchor = document.createElement('a'); 38 | anchor.href = currentHref + url; 39 | return anchor.href 40 | } 41 | 42 | const config = ${serialize(opts.uiConfig)} 43 | const resConfig = Object.assign({}, { 44 | dom_id: '#swagger-ui', 45 | deepLinking: true, 46 | presets: [ 47 | SwaggerUIBundle.presets.apis, 48 | SwaggerUIStandalonePreset 49 | ], 50 | plugins: [ 51 | SwaggerUIBundle.plugins.DownloadUrl 52 | ], 53 | layout: "StandaloneLayout", 54 | validatorUrl: ${serialize(opts.validatorUrl || null)}, 55 | }, config, { 56 | url: resolveUrl('./json'), 57 | oauth2RedirectUrl: resolveUrl('./static/oauth2-redirect.html') 58 | }); 59 | 60 | const ui = SwaggerUIBundle(resConfig) 61 | 62 | ${logoData 63 | ? ` 64 | if (resConfig.layout === 'StandaloneLayout') { 65 | // Replace the logo 66 | waitForElement('#swagger-ui > section > div.topbar > div > div > a').then((link) => { 67 | const img = document.createElement('img') 68 | img.height = 40 69 | img.src = '${logoData}' 70 | ${logoHref ? `img.href = '${logoHref}'` : 'img.href = resolveUrl(\'/\')'} 71 | ${logoTarget ? `img.target = '${logoTarget}'` : ''} 72 | link.innerHTML = '' 73 | link.appendChild(img) 74 | }) 75 | }` 76 | : ''} 77 | 78 | ui.initOAuth(${serialize(opts.initOAuth)}) 79 | }` 80 | } 81 | 82 | module.exports = swaggerInitializer 83 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 19 | 20 | 23 | 27 | 31 | 34 | 36 | 39 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/swagger-ui", 3 | "version": "5.2.3", 4 | "description": "Serve Swagger-ui for Fastify", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "prepare": "node scripts/prepare-swagger-ui", 12 | "prepublishOnly": "npm run prepare", 13 | "test": "npm run prepare && npm run unit && npm run typescript", 14 | "test:dev": "npm run lint && npm run unit && npm run typescript", 15 | "test:e2e:command": "node ./examples/example-e2e.js", 16 | "test:e2e": "npx playwright test", 17 | "test:e2e:ui": "npx playwright test --ui", 18 | "typescript": "tsd", 19 | "unit": "c8 --100 node --test" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/fastify/fastify-swagger-ui.git" 24 | }, 25 | "keywords": [ 26 | "fastify", 27 | "swagger", 28 | "openapi", 29 | "swagger-ui", 30 | "serve", 31 | "static" 32 | ], 33 | "author": "Aras Abbasi ", 34 | "contributors": [ 35 | { 36 | "name": "Matteo Collina", 37 | "email": "hello@matteocollina.com" 38 | }, 39 | { 40 | "name": "Manuel Spigolon", 41 | "email": "behemoth89@gmail.com" 42 | }, 43 | { 44 | "name": "KaKa Ng", 45 | "email": "kaka@kakang.dev", 46 | "url": "https://github.com/climba03003" 47 | }, 48 | { 49 | "name": "Frazer Smith", 50 | "email": "frazer.dev@icloud.com", 51 | "url": "https://github.com/fdawgs" 52 | } 53 | ], 54 | "license": "MIT", 55 | "bugs": { 56 | "url": "https://github.com/fastify/fastify-swagger-ui/issues" 57 | }, 58 | "homepage": "https://github.com/fastify/fastify-swagger-ui#readme", 59 | "funding": [ 60 | { 61 | "type": "github", 62 | "url": "https://github.com/sponsors/fastify" 63 | }, 64 | { 65 | "type": "opencollective", 66 | "url": "https://opencollective.com/fastify" 67 | } 68 | ], 69 | "devDependencies": { 70 | "@apidevtools/swagger-parser": "^10.1.0", 71 | "@fastify/basic-auth": "^6.0.0", 72 | "@fastify/helmet": "^13.0.0", 73 | "@fastify/pre-commit": "^2.1.0", 74 | "@fastify/swagger": "^9.0.0", 75 | "@playwright/test": "^1.43.1", 76 | "@types/node": "^22.0.0", 77 | "ajv": "^8.12.0", 78 | "c8": "^10.1.2", 79 | "eslint": "^9.17.0", 80 | "fastify": "^5.0.0", 81 | "fs-extra": "^11.2.0", 82 | "neostandard": "^0.12.0", 83 | "qs": "^6.12.1", 84 | "swagger-ui-dist": "5.21.0", 85 | "tsd": "^0.32.0" 86 | }, 87 | "dependencies": { 88 | "@fastify/static": "^8.0.0", 89 | "fastify-plugin": "^5.0.0", 90 | "openapi-types": "^12.1.3", 91 | "rfdc": "^1.3.1", 92 | "yaml": "^2.4.1" 93 | }, 94 | "tsd": { 95 | "directory": "types" 96 | }, 97 | "pkg": { 98 | "assets": [ 99 | "static/**/*" 100 | ] 101 | }, 102 | "publishConfig": { 103 | "access": "public" 104 | }, 105 | "pre-commit": [ 106 | "lint", 107 | "test" 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { defineConfig, devices } = require('@playwright/test') 4 | 5 | const PORT = 3000 6 | 7 | /** 8 | * @see https://playwright.dev/docs/test-configuration 9 | */ 10 | module.exports = defineConfig({ 11 | testDir: './e2e', 12 | fullyParallel: true, 13 | forbidOnly: !!process.env.CI, 14 | retries: process.env.CI ? 2 : 0, 15 | workers: process.env.CI ? 1 : undefined, 16 | reporter: 'html', 17 | use: { 18 | baseURL: `http://127.0.0.1:${PORT}/documentation`, 19 | trace: 'on-first-retry' 20 | }, 21 | projects: [ 22 | { 23 | name: 'chromium', 24 | use: { ...devices['Desktop Chrome'] } 25 | } 26 | ], 27 | webServer: { 28 | command: `PORT=${PORT} npm run test:e2e:command`, 29 | url: `http://127.0.0.1:${PORT}/documentation`, 30 | reuseExistingServer: !process.env.CI 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /scripts/prepare-swagger-ui.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('node:fs') 4 | const fse = require('fs-extra') 5 | const crypto = require('node:crypto') 6 | const swaggerUiAssetPath = require('swagger-ui-dist').getAbsoluteFSPath() 7 | const resolve = require('node:path').resolve 8 | 9 | const folderName = 'static' 10 | 11 | fse.emptyDirSync(resolve(`./${folderName}`)) 12 | 13 | // since the original swagger-ui-dist folder contains non UI files 14 | const filesToCopy = [ 15 | 'index.html', 16 | 'index.css', 17 | 'oauth2-redirect.html', 18 | 'swagger-ui-bundle.js', 19 | 'swagger-ui-standalone-preset.js', 20 | 'swagger-ui.css', 21 | 'swagger-ui.js' 22 | ] 23 | filesToCopy.forEach(filename => { 24 | fse.ensureFileSync(resolve(`./static/${filename}`)) 25 | const readableStream = fs.createReadStream(`${swaggerUiAssetPath}/${filename}`, 'utf8') 26 | const writableStream = fs.createWriteStream(resolve(`./static/${filename}`)) 27 | // Matches sourceMappingURL comments in .js and .css files 28 | const sourceMapRegex = new RegExp(String.raw`\/.# sourceMappingURL=${filename}.map(\*\/)?$`) 29 | 30 | readableStream.on('data', (chunk) => { 31 | // Copy file while removing sourceMappingURL comments 32 | writableStream.write(chunk.replace(sourceMapRegex, '')) 33 | }) 34 | }) 35 | 36 | const overrides = [ 37 | 'favicon-16x16.png', 38 | 'favicon-32x32.png', 39 | 'logo.svg' 40 | ] 41 | overrides.forEach(filename => { 42 | fse.copySync(`./${filename}`, resolve(`./static/${filename}`)) 43 | }) 44 | 45 | const sha = { 46 | script: [], 47 | style: [] 48 | } 49 | function computeCSPHashes (path) { 50 | const scriptRegex = /