├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── examples ├── advanced-usage.ts ├── auto-generated-openapi-docs │ ├── output │ │ └── openapi-spec.json │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── app.ts │ │ ├── generate-docs.ts │ │ ├── routes │ │ ├── todo.schema.ts │ │ └── todo.ts │ │ └── server.ts └── basic-usage.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── generate-openapi-spec.ts └── index.ts ├── tests ├── generate-openapi-spec.test.ts └── validate.test.ts └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Armagan Amcalar 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 | # echt 2 | 3 | [![npm version](https://badge.fury.io/js/echt.svg)](https://badge.fury.io/js/echt) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | Lightweight, type-safe request and response validation middleware for Express using [Zod](https://github.com/colinhacks/zod). 7 | 8 | ## Why echt? 9 | 10 | - 🎯 **True Type Safety**: Full TypeScript support with automatic type inference 11 | - 🔍 **Complete Validation**: Validate request body, headers, query parameters, URL parameters, and responses 12 | - 🪶 **Lightweight**: Zero dependencies beyond Express and Zod 13 | - 🚀 **Zero Config**: Works out of the box with TypeScript and Express 14 | - 💪 **Robust Error Handling**: Automatic error responses for invalid requests 15 | - 📕 **OpenAPI Specification**: Automatically generate OpenAPI Specification 16 | 17 | Unlike other validation libraries, echt provides complete end-to-end type safety and built-in response validation while leveraging the full power of Zod's schema validation. 18 | 19 | ## Installation 20 | 21 | ```bash 22 | npm install echt 23 | # or 24 | yarn add echt 25 | # or 26 | pnpm add echt 27 | ``` 28 | 29 | Note: `express` and `zod` are peer dependencies and must be installed separately. 30 | 31 | ## Quick Start 32 | 33 | ```typescript 34 | import express from 'express' 35 | import { validate } from 'echt' 36 | import { z } from 'zod' 37 | 38 | const app = express() 39 | app.use(express.json()) 40 | 41 | // Define your schema once, get full type safety 42 | const userSchema = { 43 | body: z.object({ 44 | name: z.string(), 45 | age: z.number() 46 | }), 47 | response: z.object({ 48 | id: z.string(), 49 | name: z.string(), 50 | age: z.number() 51 | }) 52 | } 53 | 54 | // Validation and type inference work automatically 55 | app.post('/users', validate(userSchema), (req, res) => { 56 | const { name, age } = req.body // ✅ Fully typed 57 | res.json({ id: '123', name, age }) // ✅ Response validated 58 | }) 59 | ``` 60 | 61 | ## Validation Types 62 | 63 | echt supports comprehensive validation for all parts of the request-response cycle: 64 | 65 | ### Request Validation 66 | 67 | ```typescript 68 | const schema = { 69 | // Request body validation 70 | body: z.object({ 71 | username: z.string(), 72 | age: z.number() 73 | }), 74 | 75 | // URL parameters (e.g., /users/:id) 76 | params: z.object({ 77 | id: z.string().uuid() 78 | }), 79 | 80 | // Query string parameters (e.g., ?page=1&limit=10) 81 | query: z.object({ 82 | page: z.string().transform(Number), 83 | limit: z.string().transform(Number) 84 | }), 85 | 86 | // HTTP headers 87 | headers: z.object({ 88 | 'api-key': z.string(), 89 | 'content-type': z.string().optional() 90 | }), 91 | 92 | // Express locals (useful for middleware communication) 93 | locals: z.object({ 94 | userId: z.string(), 95 | permissions: z.array(z.string()) 96 | }) 97 | } 98 | 99 | app.get('/users/:id', validate(schema), (req, res) => { 100 | // All properties are fully typed 101 | const { id } = req.params 102 | const { page, limit } = req.query 103 | const { userId } = res.locals 104 | // ... 105 | }) 106 | ``` 107 | 108 | ### Response Validation 109 | 110 | There are two ways to validate responses in echt: using `response` and `useResponse` schemas. 111 | 112 | #### Simple Response Validation 113 | 114 | The `response` schema provides basic response validation without status code differentiation: 115 | 116 | ```typescript 117 | const schema = { 118 | response: z.object({ 119 | success: z.boolean(), 120 | data: z.any() 121 | }) 122 | } 123 | 124 | app.get('/data', validate(schema), (req, res) => { 125 | res.json({ 126 | success: true, 127 | data: { 128 | /* ... */ 129 | } 130 | }) 131 | }) 132 | ``` 133 | 134 | #### Status-Aware Response Validation 135 | 136 | The `useResponse` schema provides granular control over response validation based on status codes. When using `useResponse`, you must use the `.use()` wrapper instead of passing the middleware directly: 137 | 138 | ```typescript 139 | const schema = { 140 | useResponse: { 141 | 200: z.object({ data: z.array(z.string()) }), 142 | 404: z.object({ error: z.string() }), 143 | 500: z.object({ 144 | message: z.string(), 145 | code: z.literal('INTERNAL_SERVER_ERROR'), 146 | requestId: z.string() 147 | }) 148 | } 149 | } 150 | 151 | // ❌ This will throw an error 152 | app.get('/data', validate(schema), (req, res) => {}) 153 | 154 | // ✅ This is the correct way 155 | app.get( 156 | '/data', 157 | validate(schema).use((req, res) => { 158 | if (!data) { 159 | return res.status(404).json({ error: 'Not found' }) 160 | } 161 | 162 | try { 163 | // your logic here 164 | res.json({ data: ['item1', 'item2'] }) 165 | } catch (error) { 166 | res.status(500).json({ 167 | message: 'Internal server error occurred', 168 | code: 'INTERNAL_SERVER_ERROR', 169 | requestId: generateRequestId() 170 | }) 171 | } 172 | }) 173 | ) 174 | ``` 175 | 176 | The `.use()` wrapper is required with `useResponse` because it sets up the proper type inference and validation for different status codes. Without it, the middleware won't be able to properly validate responses against their corresponding status code schemas. 177 | 178 | When using response validation, make sure to define schemas for all possible response status codes. The middleware will throw an error if you try to send a response with an undefined status code. 179 | 180 | ## OpenAPI Specification 181 | 182 | echt automatically generates an OpenAPI Specification for your API. You can access the generated specification by calling `generateOpenApiSpec(app)`. 183 | 184 | ```typescript 185 | const spec = generateOpenApiSpec(app) 186 | fs.writeFileSync('openapi-spec.json', JSON.stringify(spec, null, 2)) 187 | ``` 188 | 189 | You can check the complete example of the generated OpenAPI Specification in the [examples/auto-generated-openapi-docs](examples/auto-generated-openapi-docs) directory. 190 | 191 | ## Troubleshooting 192 | 193 | 1. **TypeScript Errors with Response Validation** 194 | If you're seeing TypeScript errors with response validation, ensure you're using the `.use()` wrapper with `useResponse` schemas. 195 | 196 | 2. **Validation Not Working** 197 | Make sure you've added `express.json()` middleware before using echt. 198 | 199 | ## Contributing 200 | 201 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 202 | 203 | 1. Fork the repository 204 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 205 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 206 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 207 | 5. Open a Pull Request 208 | 209 | ## License 210 | 211 | ``` 212 | MIT License 213 | 214 | Copyright (c) 2025 Armagan Amcalar 215 | 216 | Permission is hereby granted, free of charge, to any person obtaining a copy 217 | of this software and associated documentation files (the "Software"), to deal 218 | in the Software without restriction, including without limitation the rights 219 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 220 | copies of the Software, and to permit persons to whom the Software is 221 | furnished to do so, subject to the following conditions: 222 | 223 | The above copyright notice and this permission notice shall be included in all 224 | copies or substantial portions of the Software. 225 | 226 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 227 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 228 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 229 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 230 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 231 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 232 | SOFTWARE. 233 | ``` 234 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "correctness": { 11 | "noUnusedVariables": "error" 12 | }, 13 | "suspicious": { 14 | "noExplicitAny": "error" 15 | }, 16 | "style": { 17 | "noNonNullAssertion": "error" 18 | }, 19 | "complexity": { 20 | "noForEach": "off" 21 | } 22 | } 23 | }, 24 | "formatter": { 25 | "enabled": true, 26 | "formatWithErrors": false, 27 | "indentStyle": "space", 28 | "indentWidth": 2, 29 | "lineWidth": 100 30 | }, 31 | "javascript": { 32 | "formatter": { 33 | "quoteStyle": "single", 34 | "trailingComma": "es5", 35 | "semicolons": "asNeeded" 36 | } 37 | }, 38 | "files": { 39 | "include": ["src/**/*.ts", "tests/**/*.ts", "examples/**/*.ts"], 40 | "ignore": ["dist", "node_modules", "coverage"] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/advanced-usage.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { z } from 'zod' 3 | import { validate } from '../src' 4 | 5 | // Example usage 6 | const app = express() 7 | app.use(express.json()) 8 | 9 | const userSchema = { 10 | body: z.object({ 11 | username: z.string().min(3), 12 | email: z.string().email(), 13 | age: z.number().min(18) 14 | }), 15 | headers: z.object({ 16 | 'api-key': z.number() 17 | }), 18 | query: z.object({ 19 | include: z.string().optional() 20 | }), 21 | params: z.object({ 22 | username: z.string().optional() 23 | }), 24 | useResponse: { 25 | 201: z.object({ 26 | id: z.number(), 27 | message: z.string() 28 | }), 29 | 204: z.never(), 30 | 400: z.object({ 31 | errors: z.array( 32 | z.object({ 33 | field: z.string(), 34 | message: z.string(), 35 | code: z.string() 36 | }) 37 | ) 38 | }), 39 | 401: z.object({ 40 | message: z.string(), 41 | code: z.literal('UNAUTHORIZED') 42 | }), 43 | 403: z.object({ 44 | message: z.string(), 45 | code: z.literal('FORBIDDEN') 46 | }), 47 | 404: z.object({ 48 | message: z.string(), 49 | code: z.literal('NOT_FOUND') 50 | }), 51 | 409: z.object({ 52 | message: z.string(), 53 | code: z.literal('CONFLICT'), 54 | conflictingField: z.string() 55 | }), 56 | 422: z.object({ 57 | errors: z.array( 58 | z.object({ 59 | field: z.string(), 60 | message: z.string(), 61 | validation: z.string() 62 | }) 63 | ) 64 | }), 65 | 429: z.object({ 66 | message: z.string(), 67 | retryAfter: z.number(), 68 | code: z.literal('RATE_LIMIT_EXCEEDED') 69 | }), 70 | 500: z.object({ 71 | message: z.string(), 72 | code: z.literal('INTERNAL_SERVER_ERROR'), 73 | requestId: z.string() 74 | }) 75 | }, 76 | locals: z.object({ 77 | firtina: z.string().optional() 78 | }) 79 | } 80 | 81 | app.post( 82 | '/users/:username', 83 | validate(userSchema).use((req, res) => { 84 | const { username } = req.body 85 | 86 | // Example responses for different scenarios: 87 | if (!username) { 88 | return res.status(400).json({ 89 | errors: [ 90 | { 91 | field: 'username', 92 | message: 'Username is required', 93 | code: 'FIELD_REQUIRED' 94 | } 95 | ] 96 | }) 97 | } 98 | 99 | if (username === 'admin') { 100 | return res.status(403).json({ 101 | message: 'Cannot create user with reserved username', 102 | code: 'FORBIDDEN' 103 | }) 104 | } 105 | 106 | // Example email check 107 | if (req.body.email === 'exists@example.com') { 108 | return res.status(409).json({ 109 | message: 'User with this email already exists', 110 | code: 'CONFLICT', 111 | conflictingField: 'email' 112 | }) 113 | } 114 | 115 | try { 116 | // Simulating successful user creation 117 | return res.status(201).json({ 118 | id: 123, 119 | message: 'User created successfully' 120 | }) 121 | } catch { 122 | // Example internal server error response 123 | return res.status(500).json({ 124 | message: 'Failed to create user', 125 | code: 'INTERNAL_SERVER_ERROR', 126 | requestId: 'req_123abc' 127 | }) 128 | } 129 | }) 130 | ) 131 | 132 | // Example of a rate-limited endpoint 133 | app.get( 134 | '/users', 135 | validate({ 136 | useResponse: { 137 | 200: z.object({ 138 | data: z.array( 139 | z.object({ 140 | id: z.number(), 141 | username: z.string() 142 | }) 143 | ), 144 | pagination: z.object({ 145 | page: z.number(), 146 | limit: z.number(), 147 | total: z.number() 148 | }) 149 | }), 150 | 429: z.object({ 151 | message: z.string(), 152 | retryAfter: z.number(), 153 | code: z.literal('RATE_LIMIT_EXCEEDED') 154 | }) 155 | } 156 | }).use((_req, res) => { 157 | const isRateLimited = Math.random() > 0.8 158 | 159 | if (isRateLimited) { 160 | return res.status(429).json({ 161 | message: 'Too many requests', 162 | retryAfter: 60, 163 | code: 'RATE_LIMIT_EXCEEDED' 164 | }) 165 | } 166 | 167 | return res.status(200).json({ 168 | data: [ 169 | { id: 1, username: 'user1' }, 170 | { id: 2, username: 'user2' } 171 | ], 172 | pagination: { 173 | page: 1, 174 | limit: 10, 175 | total: 2 176 | } 177 | }) 178 | }) 179 | ) 180 | 181 | export default app 182 | -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/output/openapi-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "API", 6 | "description": "Auto Generated API by echt" 7 | }, 8 | "components": { 9 | "schemas": { 10 | "CreateTodoResponse": { 11 | "type": "object", 12 | "properties": { 13 | "id": { 14 | "type": "string", 15 | "format": "uuid" 16 | }, 17 | "title": { 18 | "type": "string", 19 | "minLength": 1 20 | }, 21 | "description": { 22 | "type": "string" 23 | } 24 | }, 25 | "required": [ 26 | "id", 27 | "title" 28 | ], 29 | "description": "The todo item was created successfully", 30 | "example": { 31 | "id": "5ee71256-98e7-4892-bae0-305b4992412c", 32 | "title": "My Todo", 33 | "description": "This is my todo" 34 | } 35 | }, 36 | "CreateTodoRequest": { 37 | "type": "object", 38 | "properties": { 39 | "title": { 40 | "type": "string", 41 | "minLength": 1 42 | }, 43 | "description": { 44 | "type": "string" 45 | } 46 | }, 47 | "required": [ 48 | "title" 49 | ] 50 | }, 51 | "GetTodosResponse": { 52 | "type": "array", 53 | "items": { 54 | "type": "object", 55 | "properties": { 56 | "id": { 57 | "type": "string", 58 | "format": "uuid" 59 | }, 60 | "title": { 61 | "type": "string", 62 | "minLength": 1 63 | }, 64 | "description": { 65 | "type": "string" 66 | } 67 | }, 68 | "required": [ 69 | "id", 70 | "title" 71 | ] 72 | } 73 | }, 74 | "GetTodoResponse": { 75 | "type": "object", 76 | "properties": { 77 | "id": { 78 | "type": "string", 79 | "format": "uuid" 80 | }, 81 | "title": { 82 | "type": "string", 83 | "minLength": 1 84 | }, 85 | "description": { 86 | "type": "string" 87 | } 88 | }, 89 | "required": [ 90 | "id", 91 | "title" 92 | ] 93 | } 94 | }, 95 | "parameters": {} 96 | }, 97 | "paths": { 98 | "/health": { 99 | "get": { 100 | "requestBody": { 101 | "content": { 102 | "application/json": { 103 | "schema": { 104 | "type": "object", 105 | "properties": { 106 | "message": { 107 | "type": "string" 108 | } 109 | }, 110 | "required": [ 111 | "message" 112 | ] 113 | } 114 | } 115 | } 116 | }, 117 | "responses": {} 118 | } 119 | }, 120 | "/todo": { 121 | "post": { 122 | "requestBody": { 123 | "content": { 124 | "application/json": { 125 | "schema": { 126 | "$ref": "#/components/schemas/CreateTodoRequest" 127 | } 128 | } 129 | } 130 | }, 131 | "responses": { 132 | "200": { 133 | "description": "Success", 134 | "content": { 135 | "application/json": { 136 | "schema": { 137 | "$ref": "#/components/schemas/CreateTodoResponse" 138 | } 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "get": { 145 | "responses": { 146 | "200": { 147 | "description": "Success", 148 | "content": { 149 | "application/json": { 150 | "schema": { 151 | "$ref": "#/components/schemas/GetTodosResponse" 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | }, 159 | "/todo/:id": { 160 | "get": { 161 | "parameters": [ 162 | { 163 | "schema": { 164 | "type": "string", 165 | "format": "uuid" 166 | }, 167 | "required": true, 168 | "name": "id", 169 | "in": "path" 170 | } 171 | ], 172 | "responses": { 173 | "200": { 174 | "description": "Success", 175 | "content": { 176 | "application/json": { 177 | "schema": { 178 | "$ref": "#/components/schemas/GetTodoResponse" 179 | } 180 | } 181 | } 182 | } 183 | } 184 | }, 185 | "put": { 186 | "responses": { 187 | "200": { 188 | "description": "200", 189 | "content": { 190 | "application/json": { 191 | "schema": { 192 | "type": "object", 193 | "properties": { 194 | "id": { 195 | "type": "string", 196 | "format": "uuid" 197 | } 198 | }, 199 | "required": [ 200 | "id" 201 | ] 202 | } 203 | } 204 | } 205 | } 206 | } 207 | }, 208 | "delete": { 209 | "responses": { 210 | "204": { 211 | "description": "204" 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "docs", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "@asteasolutions/zod-to-openapi": "^7.3.0", 12 | "swagger-ui-express": "^5.0.1" 13 | }, 14 | "devDependencies": { 15 | "ts-node": "^10.9.2" 16 | } 17 | }, 18 | "node_modules/@asteasolutions/zod-to-openapi": { 19 | "version": "7.3.0", 20 | "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.0.tgz", 21 | "integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==", 22 | "license": "MIT", 23 | "dependencies": { 24 | "openapi3-ts": "^4.1.2" 25 | }, 26 | "peerDependencies": { 27 | "zod": "^3.20.2" 28 | } 29 | }, 30 | "node_modules/@cspotcode/source-map-support": { 31 | "version": "0.8.1", 32 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 33 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 34 | "dev": true, 35 | "license": "MIT", 36 | "dependencies": { 37 | "@jridgewell/trace-mapping": "0.3.9" 38 | }, 39 | "engines": { 40 | "node": ">=12" 41 | } 42 | }, 43 | "node_modules/@jridgewell/resolve-uri": { 44 | "version": "3.1.2", 45 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 46 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 47 | "dev": true, 48 | "license": "MIT", 49 | "engines": { 50 | "node": ">=6.0.0" 51 | } 52 | }, 53 | "node_modules/@jridgewell/sourcemap-codec": { 54 | "version": "1.5.0", 55 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 56 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 57 | "dev": true, 58 | "license": "MIT" 59 | }, 60 | "node_modules/@jridgewell/trace-mapping": { 61 | "version": "0.3.9", 62 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 63 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 64 | "dev": true, 65 | "license": "MIT", 66 | "dependencies": { 67 | "@jridgewell/resolve-uri": "^3.0.3", 68 | "@jridgewell/sourcemap-codec": "^1.4.10" 69 | } 70 | }, 71 | "node_modules/@scarf/scarf": { 72 | "version": "1.4.0", 73 | "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", 74 | "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", 75 | "hasInstallScript": true, 76 | "license": "Apache-2.0" 77 | }, 78 | "node_modules/@tsconfig/node10": { 79 | "version": "1.0.11", 80 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", 81 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", 82 | "dev": true, 83 | "license": "MIT" 84 | }, 85 | "node_modules/@tsconfig/node12": { 86 | "version": "1.0.11", 87 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 88 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 89 | "dev": true, 90 | "license": "MIT" 91 | }, 92 | "node_modules/@tsconfig/node14": { 93 | "version": "1.0.3", 94 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 95 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 96 | "dev": true, 97 | "license": "MIT" 98 | }, 99 | "node_modules/@tsconfig/node16": { 100 | "version": "1.0.4", 101 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 102 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 103 | "dev": true, 104 | "license": "MIT" 105 | }, 106 | "node_modules/@types/node": { 107 | "version": "22.10.7", 108 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", 109 | "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", 110 | "dev": true, 111 | "license": "MIT", 112 | "peer": true, 113 | "dependencies": { 114 | "undici-types": "~6.20.0" 115 | } 116 | }, 117 | "node_modules/accepts": { 118 | "version": "1.3.8", 119 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 120 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 121 | "license": "MIT", 122 | "peer": true, 123 | "dependencies": { 124 | "mime-types": "~2.1.34", 125 | "negotiator": "0.6.3" 126 | }, 127 | "engines": { 128 | "node": ">= 0.6" 129 | } 130 | }, 131 | "node_modules/acorn": { 132 | "version": "8.14.0", 133 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 134 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 135 | "dev": true, 136 | "license": "MIT", 137 | "bin": { 138 | "acorn": "bin/acorn" 139 | }, 140 | "engines": { 141 | "node": ">=0.4.0" 142 | } 143 | }, 144 | "node_modules/acorn-walk": { 145 | "version": "8.3.4", 146 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", 147 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 148 | "dev": true, 149 | "license": "MIT", 150 | "dependencies": { 151 | "acorn": "^8.11.0" 152 | }, 153 | "engines": { 154 | "node": ">=0.4.0" 155 | } 156 | }, 157 | "node_modules/arg": { 158 | "version": "4.1.3", 159 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 160 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 161 | "dev": true, 162 | "license": "MIT" 163 | }, 164 | "node_modules/array-flatten": { 165 | "version": "1.1.1", 166 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 167 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 168 | "license": "MIT", 169 | "peer": true 170 | }, 171 | "node_modules/body-parser": { 172 | "version": "1.20.3", 173 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 174 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 175 | "license": "MIT", 176 | "peer": true, 177 | "dependencies": { 178 | "bytes": "3.1.2", 179 | "content-type": "~1.0.5", 180 | "debug": "2.6.9", 181 | "depd": "2.0.0", 182 | "destroy": "1.2.0", 183 | "http-errors": "2.0.0", 184 | "iconv-lite": "0.4.24", 185 | "on-finished": "2.4.1", 186 | "qs": "6.13.0", 187 | "raw-body": "2.5.2", 188 | "type-is": "~1.6.18", 189 | "unpipe": "1.0.0" 190 | }, 191 | "engines": { 192 | "node": ">= 0.8", 193 | "npm": "1.2.8000 || >= 1.4.16" 194 | } 195 | }, 196 | "node_modules/bytes": { 197 | "version": "3.1.2", 198 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 199 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 200 | "license": "MIT", 201 | "peer": true, 202 | "engines": { 203 | "node": ">= 0.8" 204 | } 205 | }, 206 | "node_modules/call-bind-apply-helpers": { 207 | "version": "1.0.1", 208 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", 209 | "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", 210 | "license": "MIT", 211 | "peer": true, 212 | "dependencies": { 213 | "es-errors": "^1.3.0", 214 | "function-bind": "^1.1.2" 215 | }, 216 | "engines": { 217 | "node": ">= 0.4" 218 | } 219 | }, 220 | "node_modules/call-bound": { 221 | "version": "1.0.3", 222 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", 223 | "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", 224 | "license": "MIT", 225 | "peer": true, 226 | "dependencies": { 227 | "call-bind-apply-helpers": "^1.0.1", 228 | "get-intrinsic": "^1.2.6" 229 | }, 230 | "engines": { 231 | "node": ">= 0.4" 232 | }, 233 | "funding": { 234 | "url": "https://github.com/sponsors/ljharb" 235 | } 236 | }, 237 | "node_modules/content-disposition": { 238 | "version": "0.5.4", 239 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 240 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 241 | "license": "MIT", 242 | "peer": true, 243 | "dependencies": { 244 | "safe-buffer": "5.2.1" 245 | }, 246 | "engines": { 247 | "node": ">= 0.6" 248 | } 249 | }, 250 | "node_modules/content-type": { 251 | "version": "1.0.5", 252 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 253 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 254 | "license": "MIT", 255 | "peer": true, 256 | "engines": { 257 | "node": ">= 0.6" 258 | } 259 | }, 260 | "node_modules/cookie": { 261 | "version": "0.7.1", 262 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 263 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 264 | "license": "MIT", 265 | "peer": true, 266 | "engines": { 267 | "node": ">= 0.6" 268 | } 269 | }, 270 | "node_modules/cookie-signature": { 271 | "version": "1.0.6", 272 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 273 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 274 | "license": "MIT", 275 | "peer": true 276 | }, 277 | "node_modules/create-require": { 278 | "version": "1.1.1", 279 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 280 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 281 | "dev": true, 282 | "license": "MIT" 283 | }, 284 | "node_modules/debug": { 285 | "version": "2.6.9", 286 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 287 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 288 | "license": "MIT", 289 | "peer": true, 290 | "dependencies": { 291 | "ms": "2.0.0" 292 | } 293 | }, 294 | "node_modules/depd": { 295 | "version": "2.0.0", 296 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 297 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 298 | "license": "MIT", 299 | "peer": true, 300 | "engines": { 301 | "node": ">= 0.8" 302 | } 303 | }, 304 | "node_modules/destroy": { 305 | "version": "1.2.0", 306 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 307 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 308 | "license": "MIT", 309 | "peer": true, 310 | "engines": { 311 | "node": ">= 0.8", 312 | "npm": "1.2.8000 || >= 1.4.16" 313 | } 314 | }, 315 | "node_modules/diff": { 316 | "version": "4.0.2", 317 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 318 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 319 | "dev": true, 320 | "license": "BSD-3-Clause", 321 | "engines": { 322 | "node": ">=0.3.1" 323 | } 324 | }, 325 | "node_modules/dunder-proto": { 326 | "version": "1.0.1", 327 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 328 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 329 | "license": "MIT", 330 | "peer": true, 331 | "dependencies": { 332 | "call-bind-apply-helpers": "^1.0.1", 333 | "es-errors": "^1.3.0", 334 | "gopd": "^1.2.0" 335 | }, 336 | "engines": { 337 | "node": ">= 0.4" 338 | } 339 | }, 340 | "node_modules/ee-first": { 341 | "version": "1.1.1", 342 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 343 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 344 | "license": "MIT", 345 | "peer": true 346 | }, 347 | "node_modules/encodeurl": { 348 | "version": "2.0.0", 349 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 350 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 351 | "license": "MIT", 352 | "peer": true, 353 | "engines": { 354 | "node": ">= 0.8" 355 | } 356 | }, 357 | "node_modules/es-define-property": { 358 | "version": "1.0.1", 359 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 360 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 361 | "license": "MIT", 362 | "peer": true, 363 | "engines": { 364 | "node": ">= 0.4" 365 | } 366 | }, 367 | "node_modules/es-errors": { 368 | "version": "1.3.0", 369 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 370 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 371 | "license": "MIT", 372 | "peer": true, 373 | "engines": { 374 | "node": ">= 0.4" 375 | } 376 | }, 377 | "node_modules/es-object-atoms": { 378 | "version": "1.1.1", 379 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 380 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 381 | "license": "MIT", 382 | "peer": true, 383 | "dependencies": { 384 | "es-errors": "^1.3.0" 385 | }, 386 | "engines": { 387 | "node": ">= 0.4" 388 | } 389 | }, 390 | "node_modules/escape-html": { 391 | "version": "1.0.3", 392 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 393 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 394 | "license": "MIT", 395 | "peer": true 396 | }, 397 | "node_modules/etag": { 398 | "version": "1.8.1", 399 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 400 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 401 | "license": "MIT", 402 | "peer": true, 403 | "engines": { 404 | "node": ">= 0.6" 405 | } 406 | }, 407 | "node_modules/express": { 408 | "version": "4.21.2", 409 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 410 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 411 | "license": "MIT", 412 | "peer": true, 413 | "dependencies": { 414 | "accepts": "~1.3.8", 415 | "array-flatten": "1.1.1", 416 | "body-parser": "1.20.3", 417 | "content-disposition": "0.5.4", 418 | "content-type": "~1.0.4", 419 | "cookie": "0.7.1", 420 | "cookie-signature": "1.0.6", 421 | "debug": "2.6.9", 422 | "depd": "2.0.0", 423 | "encodeurl": "~2.0.0", 424 | "escape-html": "~1.0.3", 425 | "etag": "~1.8.1", 426 | "finalhandler": "1.3.1", 427 | "fresh": "0.5.2", 428 | "http-errors": "2.0.0", 429 | "merge-descriptors": "1.0.3", 430 | "methods": "~1.1.2", 431 | "on-finished": "2.4.1", 432 | "parseurl": "~1.3.3", 433 | "path-to-regexp": "0.1.12", 434 | "proxy-addr": "~2.0.7", 435 | "qs": "6.13.0", 436 | "range-parser": "~1.2.1", 437 | "safe-buffer": "5.2.1", 438 | "send": "0.19.0", 439 | "serve-static": "1.16.2", 440 | "setprototypeof": "1.2.0", 441 | "statuses": "2.0.1", 442 | "type-is": "~1.6.18", 443 | "utils-merge": "1.0.1", 444 | "vary": "~1.1.2" 445 | }, 446 | "engines": { 447 | "node": ">= 0.10.0" 448 | }, 449 | "funding": { 450 | "type": "opencollective", 451 | "url": "https://opencollective.com/express" 452 | } 453 | }, 454 | "node_modules/finalhandler": { 455 | "version": "1.3.1", 456 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 457 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 458 | "license": "MIT", 459 | "peer": true, 460 | "dependencies": { 461 | "debug": "2.6.9", 462 | "encodeurl": "~2.0.0", 463 | "escape-html": "~1.0.3", 464 | "on-finished": "2.4.1", 465 | "parseurl": "~1.3.3", 466 | "statuses": "2.0.1", 467 | "unpipe": "~1.0.0" 468 | }, 469 | "engines": { 470 | "node": ">= 0.8" 471 | } 472 | }, 473 | "node_modules/forwarded": { 474 | "version": "0.2.0", 475 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 476 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 477 | "license": "MIT", 478 | "peer": true, 479 | "engines": { 480 | "node": ">= 0.6" 481 | } 482 | }, 483 | "node_modules/fresh": { 484 | "version": "0.5.2", 485 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 486 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 487 | "license": "MIT", 488 | "peer": true, 489 | "engines": { 490 | "node": ">= 0.6" 491 | } 492 | }, 493 | "node_modules/function-bind": { 494 | "version": "1.1.2", 495 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 496 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 497 | "license": "MIT", 498 | "peer": true, 499 | "funding": { 500 | "url": "https://github.com/sponsors/ljharb" 501 | } 502 | }, 503 | "node_modules/get-intrinsic": { 504 | "version": "1.2.7", 505 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", 506 | "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", 507 | "license": "MIT", 508 | "peer": true, 509 | "dependencies": { 510 | "call-bind-apply-helpers": "^1.0.1", 511 | "es-define-property": "^1.0.1", 512 | "es-errors": "^1.3.0", 513 | "es-object-atoms": "^1.0.0", 514 | "function-bind": "^1.1.2", 515 | "get-proto": "^1.0.0", 516 | "gopd": "^1.2.0", 517 | "has-symbols": "^1.1.0", 518 | "hasown": "^2.0.2", 519 | "math-intrinsics": "^1.1.0" 520 | }, 521 | "engines": { 522 | "node": ">= 0.4" 523 | }, 524 | "funding": { 525 | "url": "https://github.com/sponsors/ljharb" 526 | } 527 | }, 528 | "node_modules/get-proto": { 529 | "version": "1.0.1", 530 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 531 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 532 | "license": "MIT", 533 | "peer": true, 534 | "dependencies": { 535 | "dunder-proto": "^1.0.1", 536 | "es-object-atoms": "^1.0.0" 537 | }, 538 | "engines": { 539 | "node": ">= 0.4" 540 | } 541 | }, 542 | "node_modules/gopd": { 543 | "version": "1.2.0", 544 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 545 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 546 | "license": "MIT", 547 | "peer": true, 548 | "engines": { 549 | "node": ">= 0.4" 550 | }, 551 | "funding": { 552 | "url": "https://github.com/sponsors/ljharb" 553 | } 554 | }, 555 | "node_modules/has-symbols": { 556 | "version": "1.1.0", 557 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 558 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 559 | "license": "MIT", 560 | "peer": true, 561 | "engines": { 562 | "node": ">= 0.4" 563 | }, 564 | "funding": { 565 | "url": "https://github.com/sponsors/ljharb" 566 | } 567 | }, 568 | "node_modules/hasown": { 569 | "version": "2.0.2", 570 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 571 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 572 | "license": "MIT", 573 | "peer": true, 574 | "dependencies": { 575 | "function-bind": "^1.1.2" 576 | }, 577 | "engines": { 578 | "node": ">= 0.4" 579 | } 580 | }, 581 | "node_modules/http-errors": { 582 | "version": "2.0.0", 583 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 584 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 585 | "license": "MIT", 586 | "peer": true, 587 | "dependencies": { 588 | "depd": "2.0.0", 589 | "inherits": "2.0.4", 590 | "setprototypeof": "1.2.0", 591 | "statuses": "2.0.1", 592 | "toidentifier": "1.0.1" 593 | }, 594 | "engines": { 595 | "node": ">= 0.8" 596 | } 597 | }, 598 | "node_modules/iconv-lite": { 599 | "version": "0.4.24", 600 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 601 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 602 | "license": "MIT", 603 | "peer": true, 604 | "dependencies": { 605 | "safer-buffer": ">= 2.1.2 < 3" 606 | }, 607 | "engines": { 608 | "node": ">=0.10.0" 609 | } 610 | }, 611 | "node_modules/inherits": { 612 | "version": "2.0.4", 613 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 614 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 615 | "license": "ISC", 616 | "peer": true 617 | }, 618 | "node_modules/ipaddr.js": { 619 | "version": "1.9.1", 620 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 621 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 622 | "license": "MIT", 623 | "peer": true, 624 | "engines": { 625 | "node": ">= 0.10" 626 | } 627 | }, 628 | "node_modules/make-error": { 629 | "version": "1.3.6", 630 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 631 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 632 | "dev": true, 633 | "license": "ISC" 634 | }, 635 | "node_modules/math-intrinsics": { 636 | "version": "1.1.0", 637 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 638 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 639 | "license": "MIT", 640 | "peer": true, 641 | "engines": { 642 | "node": ">= 0.4" 643 | } 644 | }, 645 | "node_modules/media-typer": { 646 | "version": "0.3.0", 647 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 648 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 649 | "license": "MIT", 650 | "peer": true, 651 | "engines": { 652 | "node": ">= 0.6" 653 | } 654 | }, 655 | "node_modules/merge-descriptors": { 656 | "version": "1.0.3", 657 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 658 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 659 | "license": "MIT", 660 | "peer": true, 661 | "funding": { 662 | "url": "https://github.com/sponsors/sindresorhus" 663 | } 664 | }, 665 | "node_modules/methods": { 666 | "version": "1.1.2", 667 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 668 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 669 | "license": "MIT", 670 | "peer": true, 671 | "engines": { 672 | "node": ">= 0.6" 673 | } 674 | }, 675 | "node_modules/mime": { 676 | "version": "1.6.0", 677 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 678 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 679 | "license": "MIT", 680 | "peer": true, 681 | "bin": { 682 | "mime": "cli.js" 683 | }, 684 | "engines": { 685 | "node": ">=4" 686 | } 687 | }, 688 | "node_modules/mime-db": { 689 | "version": "1.52.0", 690 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 691 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 692 | "license": "MIT", 693 | "peer": true, 694 | "engines": { 695 | "node": ">= 0.6" 696 | } 697 | }, 698 | "node_modules/mime-types": { 699 | "version": "2.1.35", 700 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 701 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 702 | "license": "MIT", 703 | "peer": true, 704 | "dependencies": { 705 | "mime-db": "1.52.0" 706 | }, 707 | "engines": { 708 | "node": ">= 0.6" 709 | } 710 | }, 711 | "node_modules/ms": { 712 | "version": "2.0.0", 713 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 714 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 715 | "license": "MIT", 716 | "peer": true 717 | }, 718 | "node_modules/negotiator": { 719 | "version": "0.6.3", 720 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 721 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 722 | "license": "MIT", 723 | "peer": true, 724 | "engines": { 725 | "node": ">= 0.6" 726 | } 727 | }, 728 | "node_modules/object-inspect": { 729 | "version": "1.13.3", 730 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", 731 | "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", 732 | "license": "MIT", 733 | "peer": true, 734 | "engines": { 735 | "node": ">= 0.4" 736 | }, 737 | "funding": { 738 | "url": "https://github.com/sponsors/ljharb" 739 | } 740 | }, 741 | "node_modules/on-finished": { 742 | "version": "2.4.1", 743 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 744 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 745 | "license": "MIT", 746 | "peer": true, 747 | "dependencies": { 748 | "ee-first": "1.1.1" 749 | }, 750 | "engines": { 751 | "node": ">= 0.8" 752 | } 753 | }, 754 | "node_modules/openapi3-ts": { 755 | "version": "4.4.0", 756 | "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", 757 | "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", 758 | "license": "MIT", 759 | "dependencies": { 760 | "yaml": "^2.5.0" 761 | } 762 | }, 763 | "node_modules/parseurl": { 764 | "version": "1.3.3", 765 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 766 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 767 | "license": "MIT", 768 | "peer": true, 769 | "engines": { 770 | "node": ">= 0.8" 771 | } 772 | }, 773 | "node_modules/path-to-regexp": { 774 | "version": "0.1.12", 775 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 776 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 777 | "license": "MIT", 778 | "peer": true 779 | }, 780 | "node_modules/proxy-addr": { 781 | "version": "2.0.7", 782 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 783 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 784 | "license": "MIT", 785 | "peer": true, 786 | "dependencies": { 787 | "forwarded": "0.2.0", 788 | "ipaddr.js": "1.9.1" 789 | }, 790 | "engines": { 791 | "node": ">= 0.10" 792 | } 793 | }, 794 | "node_modules/qs": { 795 | "version": "6.13.0", 796 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 797 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 798 | "license": "BSD-3-Clause", 799 | "peer": true, 800 | "dependencies": { 801 | "side-channel": "^1.0.6" 802 | }, 803 | "engines": { 804 | "node": ">=0.6" 805 | }, 806 | "funding": { 807 | "url": "https://github.com/sponsors/ljharb" 808 | } 809 | }, 810 | "node_modules/range-parser": { 811 | "version": "1.2.1", 812 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 813 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 814 | "license": "MIT", 815 | "peer": true, 816 | "engines": { 817 | "node": ">= 0.6" 818 | } 819 | }, 820 | "node_modules/raw-body": { 821 | "version": "2.5.2", 822 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 823 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 824 | "license": "MIT", 825 | "peer": true, 826 | "dependencies": { 827 | "bytes": "3.1.2", 828 | "http-errors": "2.0.0", 829 | "iconv-lite": "0.4.24", 830 | "unpipe": "1.0.0" 831 | }, 832 | "engines": { 833 | "node": ">= 0.8" 834 | } 835 | }, 836 | "node_modules/safe-buffer": { 837 | "version": "5.2.1", 838 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 839 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 840 | "funding": [ 841 | { 842 | "type": "github", 843 | "url": "https://github.com/sponsors/feross" 844 | }, 845 | { 846 | "type": "patreon", 847 | "url": "https://www.patreon.com/feross" 848 | }, 849 | { 850 | "type": "consulting", 851 | "url": "https://feross.org/support" 852 | } 853 | ], 854 | "license": "MIT", 855 | "peer": true 856 | }, 857 | "node_modules/safer-buffer": { 858 | "version": "2.1.2", 859 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 860 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 861 | "license": "MIT", 862 | "peer": true 863 | }, 864 | "node_modules/send": { 865 | "version": "0.19.0", 866 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 867 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 868 | "license": "MIT", 869 | "peer": true, 870 | "dependencies": { 871 | "debug": "2.6.9", 872 | "depd": "2.0.0", 873 | "destroy": "1.2.0", 874 | "encodeurl": "~1.0.2", 875 | "escape-html": "~1.0.3", 876 | "etag": "~1.8.1", 877 | "fresh": "0.5.2", 878 | "http-errors": "2.0.0", 879 | "mime": "1.6.0", 880 | "ms": "2.1.3", 881 | "on-finished": "2.4.1", 882 | "range-parser": "~1.2.1", 883 | "statuses": "2.0.1" 884 | }, 885 | "engines": { 886 | "node": ">= 0.8.0" 887 | } 888 | }, 889 | "node_modules/send/node_modules/encodeurl": { 890 | "version": "1.0.2", 891 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 892 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 893 | "license": "MIT", 894 | "peer": true, 895 | "engines": { 896 | "node": ">= 0.8" 897 | } 898 | }, 899 | "node_modules/send/node_modules/ms": { 900 | "version": "2.1.3", 901 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 902 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 903 | "license": "MIT", 904 | "peer": true 905 | }, 906 | "node_modules/serve-static": { 907 | "version": "1.16.2", 908 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 909 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 910 | "license": "MIT", 911 | "peer": true, 912 | "dependencies": { 913 | "encodeurl": "~2.0.0", 914 | "escape-html": "~1.0.3", 915 | "parseurl": "~1.3.3", 916 | "send": "0.19.0" 917 | }, 918 | "engines": { 919 | "node": ">= 0.8.0" 920 | } 921 | }, 922 | "node_modules/setprototypeof": { 923 | "version": "1.2.0", 924 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 925 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 926 | "license": "ISC", 927 | "peer": true 928 | }, 929 | "node_modules/side-channel": { 930 | "version": "1.1.0", 931 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 932 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 933 | "license": "MIT", 934 | "peer": true, 935 | "dependencies": { 936 | "es-errors": "^1.3.0", 937 | "object-inspect": "^1.13.3", 938 | "side-channel-list": "^1.0.0", 939 | "side-channel-map": "^1.0.1", 940 | "side-channel-weakmap": "^1.0.2" 941 | }, 942 | "engines": { 943 | "node": ">= 0.4" 944 | }, 945 | "funding": { 946 | "url": "https://github.com/sponsors/ljharb" 947 | } 948 | }, 949 | "node_modules/side-channel-list": { 950 | "version": "1.0.0", 951 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 952 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 953 | "license": "MIT", 954 | "peer": true, 955 | "dependencies": { 956 | "es-errors": "^1.3.0", 957 | "object-inspect": "^1.13.3" 958 | }, 959 | "engines": { 960 | "node": ">= 0.4" 961 | }, 962 | "funding": { 963 | "url": "https://github.com/sponsors/ljharb" 964 | } 965 | }, 966 | "node_modules/side-channel-map": { 967 | "version": "1.0.1", 968 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 969 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 970 | "license": "MIT", 971 | "peer": true, 972 | "dependencies": { 973 | "call-bound": "^1.0.2", 974 | "es-errors": "^1.3.0", 975 | "get-intrinsic": "^1.2.5", 976 | "object-inspect": "^1.13.3" 977 | }, 978 | "engines": { 979 | "node": ">= 0.4" 980 | }, 981 | "funding": { 982 | "url": "https://github.com/sponsors/ljharb" 983 | } 984 | }, 985 | "node_modules/side-channel-weakmap": { 986 | "version": "1.0.2", 987 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 988 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 989 | "license": "MIT", 990 | "peer": true, 991 | "dependencies": { 992 | "call-bound": "^1.0.2", 993 | "es-errors": "^1.3.0", 994 | "get-intrinsic": "^1.2.5", 995 | "object-inspect": "^1.13.3", 996 | "side-channel-map": "^1.0.1" 997 | }, 998 | "engines": { 999 | "node": ">= 0.4" 1000 | }, 1001 | "funding": { 1002 | "url": "https://github.com/sponsors/ljharb" 1003 | } 1004 | }, 1005 | "node_modules/statuses": { 1006 | "version": "2.0.1", 1007 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1008 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1009 | "license": "MIT", 1010 | "peer": true, 1011 | "engines": { 1012 | "node": ">= 0.8" 1013 | } 1014 | }, 1015 | "node_modules/swagger-ui-dist": { 1016 | "version": "5.18.2", 1017 | "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", 1018 | "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", 1019 | "license": "Apache-2.0", 1020 | "dependencies": { 1021 | "@scarf/scarf": "=1.4.0" 1022 | } 1023 | }, 1024 | "node_modules/swagger-ui-express": { 1025 | "version": "5.0.1", 1026 | "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", 1027 | "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", 1028 | "license": "MIT", 1029 | "dependencies": { 1030 | "swagger-ui-dist": ">=5.0.0" 1031 | }, 1032 | "engines": { 1033 | "node": ">= v0.10.32" 1034 | }, 1035 | "peerDependencies": { 1036 | "express": ">=4.0.0 || >=5.0.0-beta" 1037 | } 1038 | }, 1039 | "node_modules/toidentifier": { 1040 | "version": "1.0.1", 1041 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1042 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1043 | "license": "MIT", 1044 | "peer": true, 1045 | "engines": { 1046 | "node": ">=0.6" 1047 | } 1048 | }, 1049 | "node_modules/ts-node": { 1050 | "version": "10.9.2", 1051 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", 1052 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 1053 | "dev": true, 1054 | "license": "MIT", 1055 | "dependencies": { 1056 | "@cspotcode/source-map-support": "^0.8.0", 1057 | "@tsconfig/node10": "^1.0.7", 1058 | "@tsconfig/node12": "^1.0.7", 1059 | "@tsconfig/node14": "^1.0.0", 1060 | "@tsconfig/node16": "^1.0.2", 1061 | "acorn": "^8.4.1", 1062 | "acorn-walk": "^8.1.1", 1063 | "arg": "^4.1.0", 1064 | "create-require": "^1.1.0", 1065 | "diff": "^4.0.1", 1066 | "make-error": "^1.1.1", 1067 | "v8-compile-cache-lib": "^3.0.1", 1068 | "yn": "3.1.1" 1069 | }, 1070 | "bin": { 1071 | "ts-node": "dist/bin.js", 1072 | "ts-node-cwd": "dist/bin-cwd.js", 1073 | "ts-node-esm": "dist/bin-esm.js", 1074 | "ts-node-script": "dist/bin-script.js", 1075 | "ts-node-transpile-only": "dist/bin-transpile.js", 1076 | "ts-script": "dist/bin-script-deprecated.js" 1077 | }, 1078 | "peerDependencies": { 1079 | "@swc/core": ">=1.2.50", 1080 | "@swc/wasm": ">=1.2.50", 1081 | "@types/node": "*", 1082 | "typescript": ">=2.7" 1083 | }, 1084 | "peerDependenciesMeta": { 1085 | "@swc/core": { 1086 | "optional": true 1087 | }, 1088 | "@swc/wasm": { 1089 | "optional": true 1090 | } 1091 | } 1092 | }, 1093 | "node_modules/type-is": { 1094 | "version": "1.6.18", 1095 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1096 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1097 | "license": "MIT", 1098 | "peer": true, 1099 | "dependencies": { 1100 | "media-typer": "0.3.0", 1101 | "mime-types": "~2.1.24" 1102 | }, 1103 | "engines": { 1104 | "node": ">= 0.6" 1105 | } 1106 | }, 1107 | "node_modules/typescript": { 1108 | "version": "5.7.3", 1109 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", 1110 | "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", 1111 | "dev": true, 1112 | "license": "Apache-2.0", 1113 | "peer": true, 1114 | "bin": { 1115 | "tsc": "bin/tsc", 1116 | "tsserver": "bin/tsserver" 1117 | }, 1118 | "engines": { 1119 | "node": ">=14.17" 1120 | } 1121 | }, 1122 | "node_modules/undici-types": { 1123 | "version": "6.20.0", 1124 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", 1125 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", 1126 | "dev": true, 1127 | "license": "MIT", 1128 | "peer": true 1129 | }, 1130 | "node_modules/unpipe": { 1131 | "version": "1.0.0", 1132 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1133 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1134 | "license": "MIT", 1135 | "peer": true, 1136 | "engines": { 1137 | "node": ">= 0.8" 1138 | } 1139 | }, 1140 | "node_modules/utils-merge": { 1141 | "version": "1.0.1", 1142 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1143 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1144 | "license": "MIT", 1145 | "peer": true, 1146 | "engines": { 1147 | "node": ">= 0.4.0" 1148 | } 1149 | }, 1150 | "node_modules/v8-compile-cache-lib": { 1151 | "version": "3.0.1", 1152 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 1153 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 1154 | "dev": true, 1155 | "license": "MIT" 1156 | }, 1157 | "node_modules/vary": { 1158 | "version": "1.1.2", 1159 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1160 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1161 | "license": "MIT", 1162 | "peer": true, 1163 | "engines": { 1164 | "node": ">= 0.8" 1165 | } 1166 | }, 1167 | "node_modules/yaml": { 1168 | "version": "2.7.0", 1169 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", 1170 | "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", 1171 | "license": "ISC", 1172 | "bin": { 1173 | "yaml": "bin.mjs" 1174 | }, 1175 | "engines": { 1176 | "node": ">= 14" 1177 | } 1178 | }, 1179 | "node_modules/yn": { 1180 | "version": "3.1.1", 1181 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 1182 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 1183 | "dev": true, 1184 | "license": "MIT", 1185 | "engines": { 1186 | "node": ">=6" 1187 | } 1188 | }, 1189 | "node_modules/zod": { 1190 | "version": "3.24.1", 1191 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", 1192 | "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", 1193 | "license": "MIT", 1194 | "peer": true, 1195 | "funding": { 1196 | "url": "https://github.com/sponsors/colinhacks" 1197 | } 1198 | } 1199 | } 1200 | } 1201 | -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "ts-node src/server.ts", 7 | "generate-docs": "ts-node src/generate-docs.ts" 8 | }, 9 | "dependencies": { 10 | "@asteasolutions/zod-to-openapi": "^7.3.0", 11 | "swagger-ui-express": "^5.0.1" 12 | }, 13 | "devDependencies": { 14 | "ts-node": "^10.9.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | // putting a ts ignore here because swagger-ui-express types are conflicting with express types we use for echt 5 | // @ts-ignore 6 | import swaggerUi from 'swagger-ui-express' 7 | import { z } from 'zod' 8 | import todo from './routes/todo' 9 | import { validate } from '../../../src' 10 | 11 | const openapiSpecContent = fs.readFileSync( 12 | path.resolve(__dirname, '..', 'output', './openapi-spec.json'), 13 | 'utf8' 14 | ) 15 | const openapiSpec = JSON.parse(openapiSpecContent) 16 | 17 | const app = express() 18 | 19 | // Regular route 20 | app.get( 21 | '/health', 22 | validate({ 23 | body: z.object({ 24 | message: z.string(), 25 | }), 26 | }), 27 | (_req, res) => { 28 | res.json({ message: 'Hello World' }) 29 | } 30 | ) 31 | 32 | // Using a router 33 | app.use('/todo', todo) 34 | 35 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec)) 36 | 37 | export default app 38 | -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/src/generate-docs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import app from './app' 4 | import { generateOpenApiSpec } from '../../../src' 5 | 6 | const spec = generateOpenApiSpec(app) 7 | 8 | fs.writeFileSync( 9 | path.resolve(__dirname, '..', 'output', 'openapi-spec.json'), 10 | JSON.stringify(spec, null, 2) 11 | ) 12 | -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/src/routes/todo.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi' 3 | 4 | extendZodWithOpenApi(z) 5 | 6 | const uuidExample = '5ee71256-98e7-4892-bae0-305b4992412c' 7 | 8 | export const createTodoRequestBody = z 9 | .object({ 10 | title: z.string().trim().min(1), 11 | description: z.string().trim().optional(), 12 | }) 13 | .openapi('CreateTodoRequest') 14 | 15 | export const createTodoResponse = z 16 | .object({ 17 | id: z.string().uuid(), 18 | title: z.string().trim().min(1), 19 | description: z.string().trim().optional(), 20 | }) 21 | .openapi('CreateTodoResponse') 22 | .openapi({ 23 | description: 'The todo item was created successfully', 24 | example: { 25 | id: uuidExample, 26 | title: 'My Todo', 27 | description: 'This is my todo', 28 | }, 29 | }) 30 | 31 | export const getTodoResponse = z 32 | .object({ 33 | id: z.string().uuid(), 34 | title: z.string().trim().min(1), 35 | description: z.string().trim().optional(), 36 | }) 37 | .openapi('GetTodoResponse') 38 | 39 | export const getTodosResponse = z 40 | .array( 41 | z.object({ 42 | id: z.string().uuid(), 43 | title: z.string().trim().min(1), 44 | description: z.string().trim().optional(), 45 | }) 46 | ) 47 | .openapi('GetTodosResponse') 48 | -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/src/routes/todo.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | import { Router } from 'express' 3 | import { z } from 'zod' 4 | 5 | import { validate } from '../../../../src' 6 | import { 7 | createTodoRequestBody, 8 | createTodoResponse, 9 | getTodoResponse, 10 | getTodosResponse, 11 | } from './todo.schema' 12 | 13 | const router = Router() 14 | 15 | router.post( 16 | '/', 17 | validate({ 18 | body: createTodoRequestBody, 19 | response: createTodoResponse, 20 | }), 21 | (req, res) => { 22 | res.json({ 23 | id: randomUUID(), 24 | title: req.body.title, 25 | description: req.body.description, 26 | }) 27 | } 28 | ) 29 | 30 | router.get( 31 | '/', 32 | validate({ 33 | response: getTodosResponse, 34 | }), 35 | (_req, res) => { 36 | res.json([ 37 | { 38 | id: randomUUID(), 39 | title: 'Todo 1', 40 | description: 'Todo 1 description', 41 | }, 42 | ]) 43 | } 44 | ) 45 | 46 | router.get( 47 | '/:id', 48 | validate({ 49 | params: z.object({ 50 | id: z.string().uuid(), 51 | }), 52 | response: getTodoResponse, 53 | }), 54 | (_req, res) => { 55 | res.json({ 56 | id: randomUUID(), 57 | title: 'Todo 1', 58 | description: 'Todo 1 description', 59 | }) 60 | } 61 | ) 62 | 63 | router.put( 64 | '/:id', 65 | validate({ 66 | useResponse: { 67 | 200: z.object({ 68 | id: z.string().uuid(), 69 | }), 70 | }, 71 | }).use((_req, res) => { 72 | res.status(200).json({ 73 | id: randomUUID(), 74 | }) 75 | }) 76 | ) 77 | 78 | router.delete( 79 | '/:id', 80 | validate({ 81 | useResponse: { 82 | 204: z.never(), 83 | }, 84 | }).use((_req, res) => { 85 | res.sendStatus(204) 86 | }) 87 | ) 88 | 89 | export default router 90 | -------------------------------------------------------------------------------- /examples/auto-generated-openapi-docs/src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app' 2 | 3 | app.listen(3000, () => { 4 | console.log('Server is running on port 3000') 5 | }) 6 | -------------------------------------------------------------------------------- /examples/basic-usage.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { z } from 'zod' 3 | import { validate } from '../src' 4 | 5 | const app = express() 6 | app.use(express.json()) 7 | 8 | const userSchema = { 9 | body: z.object({ 10 | username: z.string().min(3), 11 | email: z.string().email(), 12 | age: z.number().min(18) 13 | }), 14 | headers: z.object({ 15 | 'api-key': z.number() 16 | }), 17 | query: z.object({ 18 | include: z.string().optional() 19 | }), 20 | params: z.object({ 21 | username: z.string().optional() 22 | }), 23 | response: z.object({ 24 | result: z.string().optional() 25 | }), 26 | locals: z.object({ 27 | account: z.string().optional() 28 | }) 29 | } 30 | 31 | app.post('/users/:username', validate(userSchema), (req, res) => { 32 | // Types are automatically inferred 33 | const { username, email, age } = req.body 34 | res.send({ result: 'ok' }) 35 | 36 | res.send({ 37 | result: `Created user ${username} (${email}) aged ${age}` 38 | }) 39 | }) 40 | 41 | export default app 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/tests/**/*.test.ts'], 5 | collectCoverage: true, 6 | coverageDirectory: 'coverage', 7 | coverageProvider: 'v8', 8 | coverageReporters: ['text', 'lcov', 'clover', 'html'], 9 | coverageThreshold: { 10 | global: { 11 | branches: 80, 12 | functions: 80, 13 | lines: 80, 14 | statements: 80 15 | } 16 | }, 17 | collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.test.ts'] 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echt", 3 | "version": "2.0.2", 4 | "description": "Lightweight, type-safe request validation middleware for Express using Zod", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "jest", 10 | "test:coverage": "jest --coverage", 11 | "test:watch": "jest --watch", 12 | "lint": "biome check .", 13 | "lint:fix": "biome check --apply .", 14 | "format": "biome format .", 15 | "format:fix": "biome format --write .", 16 | "prepare": "npm run build", 17 | "prepublishOnly": "npm test && npm run lint", 18 | "preversion": "npm run lint", 19 | "version": "npm run format:fix && git add -A src", 20 | "postversion": "git push && git push --tags" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/dashersw/echt.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/dashersw/echt/issues" 28 | }, 29 | "homepage": "https://github.com/dashersw/echt#readme", 30 | "keywords": [ 31 | "express", 32 | "zod", 33 | "validation", 34 | "middleware", 35 | "typescript", 36 | "request-validation", 37 | "schema-validation", 38 | "type-safe" 39 | ], 40 | "files": [ 41 | "dist", 42 | "LICENSE", 43 | "README.md" 44 | ], 45 | "author": "", 46 | "license": "MIT", 47 | "dependencies": { 48 | "express": "^4.x", 49 | "zod": "^3.x" 50 | }, 51 | "devDependencies": { 52 | "@biomejs/biome": "1.5.3", 53 | "@types/express": "^4.17.21", 54 | "@types/jest": "^29.5.11", 55 | "@types/node": "^20.10.5", 56 | "@types/supertest": "^2.0.16", 57 | "jest": "^29.7.0", 58 | "openapi-types": "^12.1.3", 59 | "supertest": "^6.3.3", 60 | "ts-jest": "^29.1.1", 61 | "typescript": "^5.3.3", 62 | "zod-to-json-schema": "^3.24.1" 63 | }, 64 | "peerDependencies": { 65 | "@asteasolutions/zod-to-openapi": "^7.x", 66 | "express": "^4.x", 67 | "zod": "^3.x" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/generate-openapi-spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenAPIRegistry, 3 | OpenApiGeneratorV3, 4 | type RouteConfig, 5 | type ResponseConfig, 6 | } from '@asteasolutions/zod-to-openapi' 7 | import { type AnyZodObject } from 'zod' 8 | import { type Express } from 'express' 9 | 10 | import type { 11 | SimpleResponseValidationSchema, 12 | TypedResponseValidationSchema, 13 | ValidationSchema, 14 | } from '.' 15 | import { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator' 16 | 17 | // biome-ignore lint/suspicious/noExplicitAny: 18 | type SafeAny = any 19 | 20 | function generateRouteConfig( 21 | method: RouteConfig['method'], 22 | path: string, 23 | schemas_: ValidationSchema 24 | ) { 25 | const schemas = 26 | 'useResponse' in schemas_ 27 | ? (schemas_ as TypedResponseValidationSchema) 28 | : (schemas_ as SimpleResponseValidationSchema) 29 | 30 | const route: RouteConfig = { 31 | method: method, 32 | path: path, 33 | responses: {}, 34 | } 35 | 36 | route.request = {} 37 | 38 | if (schemas.body) { 39 | route.request.body = { 40 | content: { 41 | 'application/json': { 42 | schema: schemas.body, 43 | }, 44 | }, 45 | } 46 | } 47 | 48 | if (schemas.params) { 49 | route.request.params = schemas.params as AnyZodObject 50 | } 51 | 52 | if (schemas.query) { 53 | route.request.query = schemas.query as AnyZodObject 54 | } 55 | 56 | if (schemas.headers) { 57 | route.request.headers = schemas.headers as AnyZodObject 58 | } 59 | 60 | if (schemas.response) { 61 | route.responses = { 62 | '200': { 63 | description: 'Success', 64 | content: { 65 | 'application/json': { 66 | schema: schemas.response, 67 | }, 68 | }, 69 | }, 70 | } 71 | } else if (schemas.useResponse) { 72 | const responses = Object.entries(schemas.useResponse) 73 | 74 | responses.forEach(([status, schema]) => { 75 | let content: ResponseConfig['content'] = { 76 | 'application/json': { 77 | schema: schema, 78 | }, 79 | } 80 | 81 | // biome-ignore lint/suspicious/noExplicitAny: typeName actually exists but type got overridden by @asteasolutions/zod-to-openapi 82 | const typeName = (schema as any)._def.typeName 83 | 84 | if (typeName === 'ZodNever') { 85 | content = undefined 86 | } 87 | 88 | route.responses[status] = { 89 | description: status, 90 | content, 91 | } 92 | }) 93 | } 94 | 95 | return route 96 | } 97 | 98 | function processStack( 99 | stack: Express['_router']['stack'], 100 | registry: OpenAPIRegistry, 101 | basePath = '' 102 | ) { 103 | const routerLayers = stack.filter((layer: SafeAny) => layer.name === 'router') 104 | const layersWithRoutes = stack.filter((layer: SafeAny) => layer.route) 105 | 106 | layersWithRoutes.forEach((layer: SafeAny) => { 107 | const routePath = basePath + layer.route.path 108 | const layersWithSchemas = layer.route.stack.filter( 109 | (layer: SafeAny) => 'schemas' in layer.handle 110 | ) 111 | 112 | layersWithSchemas.forEach((layer: SafeAny) => { 113 | const path = routePath.replace(/\/$/, '') 114 | const route = generateRouteConfig(layer.method, path, layer.handle.schemas) 115 | 116 | registry.registerPath(route) 117 | }) 118 | }) 119 | 120 | routerLayers.forEach((layer: SafeAny) => { 121 | // Recursively process sub-routers 122 | const routerPath = layer.regexp 123 | .toString() 124 | .replace('/^', '') 125 | .replace('/?(?=\\/|$)/i', '') 126 | .replace(/\\/g, '') 127 | 128 | const newBasePath = basePath + routerPath 129 | processStack(layer.handle.stack, registry, newBasePath) 130 | }) 131 | } 132 | 133 | export function generateOpenApiSpec( 134 | app: Express, 135 | openapiObjectConfig: OpenAPIObjectConfig = { 136 | openapi: '3.0.0', 137 | info: { 138 | version: '1.0.0', 139 | title: 'API', 140 | description: 'Auto Generated API by echt', 141 | }, 142 | } 143 | ) { 144 | const registry = new OpenAPIRegistry() 145 | 146 | processStack(app._router.stack, registry) 147 | 148 | const generator = new OpenApiGeneratorV3(registry.definitions) 149 | 150 | const document = generator.generateDocument(openapiObjectConfig) 151 | 152 | return document 153 | } 154 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction, RequestHandler } from 'express' 2 | import { ZodError, type z } from 'zod' 3 | 4 | type StatusCodes = 5 | | 100 // Continue 6 | | 101 // Switching Protocols 7 | | 102 // Processing 8 | | 103 // Early Hints 9 | | 200 // OK 10 | | 201 // Created 11 | | 202 // Accepted 12 | | 203 // Non-Authoritative Information 13 | | 204 // No Content 14 | | 205 // Reset Content 15 | | 206 // Partial Content 16 | | 207 // Multi-Status 17 | | 208 // Already Reported 18 | | 226 // IM Used 19 | | 300 // Multiple Choices 20 | | 301 // Moved Permanently 21 | | 302 // Found 22 | | 303 // See Other 23 | | 304 // Not Modified 24 | | 305 // Use Proxy 25 | | 307 // Temporary Redirect 26 | | 308 // Permanent Redirect 27 | | 400 // Bad Request 28 | | 401 // Unauthorized 29 | | 402 // Payment Required 30 | | 403 // Forbidden 31 | | 404 // Not Found 32 | | 405 // Method Not Allowed 33 | | 406 // Not Acceptable 34 | | 407 // Proxy Authentication Required 35 | | 408 // Request Timeout 36 | | 409 // Conflict 37 | | 410 // Gone 38 | | 411 // Length Required 39 | | 412 // Precondition Failed 40 | | 413 // Payload Too Large 41 | | 414 // URI Too Long 42 | | 415 // Unsupported Media Type 43 | | 416 // Range Not Satisfiable 44 | | 417 // Expectation Failed 45 | | 418 // I'm a teapot 46 | | 421 // Misdirected Request 47 | | 422 // Unprocessable Entity 48 | | 423 // Locked 49 | | 424 // Failed Dependency 50 | | 425 // Too Early 51 | | 426 // Upgrade Required 52 | | 428 // Precondition Required 53 | | 429 // Too Many Requests 54 | | 431 // Request Header Fields Too Large 55 | | 451 // Unavailable For Legal Reasons 56 | | 500 // Internal Server Error 57 | | 501 // Not Implemented 58 | | 502 // Bad Gateway 59 | | 503 // Service Unavailable 60 | | 504 // Gateway Timeout 61 | | 505 // HTTP Version Not Supported 62 | | 506 // Variant Also Negotiates 63 | | 507 // Insufficient Storage 64 | | 508 // Loop Detected 65 | | 509 // Bandwidth Limit Exceeded 66 | | 510 // Not Extended 67 | | 511 // Network Authentication Required 68 | 69 | type TypedResponse>> = T 70 | 71 | type BaseValidationSchema = { 72 | body?: z.ZodType 73 | headers?: z.ZodType 74 | query?: z.ZodType 75 | params?: z.ZodType 76 | locals?: z.ZodType 77 | } 78 | 79 | export type SimpleResponseValidationSchema = BaseValidationSchema & { 80 | response: z.ZodType 81 | useResponse?: never 82 | } 83 | 84 | export type TypedResponseValidationSchema = BaseValidationSchema & { 85 | useResponse: TypedResponse>> 86 | response?: never 87 | } 88 | 89 | export type ValidationSchema = BaseValidationSchema | SimpleResponseValidationSchema | TypedResponseValidationSchema 90 | 91 | type InferSchemaType = { 92 | body: T['body'] extends z.ZodType ? z.infer : unknown 93 | headers: T['headers'] extends z.ZodType ? z.infer : unknown 94 | query: T['query'] extends z.ZodType ? z.infer : unknown 95 | params: T['params'] extends z.ZodType ? z.infer : unknown 96 | response: T extends SimpleResponseValidationSchema ? z.infer : unknown 97 | locals: T['locals'] extends z.ZodType ? z.infer : Record 98 | } 99 | 100 | type ValidatedRequest = Request< 101 | InferSchemaType['params'], 102 | InferSchemaType['response'], 103 | InferSchemaType['body'], 104 | InferSchemaType['query'], 105 | InferSchemaType['locals'] 106 | > 107 | 108 | type TypedJsonResponse = T extends TypedResponseValidationSchema 109 | ? Status extends keyof T['useResponse'] 110 | ? T['useResponse'][Status] extends z.ZodType 111 | ? z.infer 112 | : never 113 | : never 114 | : never 115 | 116 | type ValidatedResponse = Omit & { 117 | req: Request 118 | status( 119 | code: S 120 | ): Omit, 'status'> & { 121 | json(body: TypedJsonResponse): void 122 | send(body: TypedJsonResponse): void 123 | } 124 | json(body: TypedJsonResponse): void 125 | send(body: TypedJsonResponse): void 126 | } 127 | 128 | type ValidatedMiddleware = RequestHandler< 129 | InferSchemaType['params'], 130 | InferSchemaType['response'], 131 | InferSchemaType['body'], 132 | InferSchemaType['query'], 133 | InferSchemaType['locals'] 134 | > & { 135 | schemas: T 136 | handler: RequestHandler< 137 | InferSchemaType['params'], 138 | InferSchemaType['response'], 139 | InferSchemaType['body'], 140 | InferSchemaType['query'], 141 | InferSchemaType['locals'] 142 | > 143 | use: T extends TypedResponseValidationSchema 144 | ?

, res: ValidatedResponse, next: NextFunction) => void>( 145 | handler: P 146 | ) => RequestHandler & { schemas: T } 147 | : never 148 | } 149 | 150 | export const validate = ( 151 | schemas: T 152 | ): T extends TypedResponseValidationSchema 153 | ? Omit, 'handler' | 'schemas'> 154 | : ValidatedMiddleware => { 155 | const middleware = (req: ValidatedRequest, res: ValidatedResponse, next: NextFunction) => { 156 | const validationErrors: z.ZodError[] = [] 157 | 158 | try { 159 | if ('headers' in schemas && schemas.headers) { 160 | const headerData = Object.fromEntries( 161 | Object.entries(req.headers).map(([key, value]) => { 162 | const lowercaseKey = key.toLowerCase() 163 | let headerValue: string | undefined 164 | if (typeof value === 'string') { 165 | headerValue = value 166 | } else { 167 | if (value) { 168 | headerValue = value[0] 169 | } else { 170 | headerValue = undefined 171 | } 172 | } 173 | return [lowercaseKey, headerValue] 174 | }) 175 | ) 176 | try { 177 | const result = schemas.headers.parse(headerData) 178 | Object.assign(req.headers, result) 179 | } catch (error) { 180 | if (error instanceof ZodError) { 181 | validationErrors.push(error) 182 | } else { 183 | throw error 184 | } 185 | } 186 | } 187 | 188 | if ('body' in schemas && schemas.body) { 189 | try { 190 | const result = schemas.body.parse(req.body) 191 | req.body = result 192 | } catch (error) { 193 | if (error instanceof ZodError) { 194 | validationErrors.push(error) 195 | } else { 196 | throw error 197 | } 198 | } 199 | } 200 | 201 | if ('query' in schemas && schemas.query) { 202 | try { 203 | const result = schemas.query.parse(req.query) 204 | req.query = result 205 | } catch (error) { 206 | if (error instanceof ZodError) { 207 | validationErrors.push(error) 208 | } else { 209 | throw error 210 | } 211 | } 212 | } 213 | 214 | if ('params' in schemas && schemas.params) { 215 | try { 216 | const result = schemas.params.parse(req.params) 217 | req.params = result 218 | } catch (error) { 219 | if (error instanceof ZodError) { 220 | validationErrors.push(error) 221 | } else { 222 | throw error 223 | } 224 | } 225 | } 226 | 227 | if ('locals' in schemas && schemas.locals) { 228 | try { 229 | const result = schemas.locals.parse(res.locals) 230 | res.locals = result 231 | } catch (error) { 232 | if (error instanceof ZodError) { 233 | validationErrors.push(error) 234 | } else { 235 | throw error 236 | } 237 | } 238 | } 239 | 240 | if (validationErrors.length > 0) { 241 | const allErrors = validationErrors.flatMap(error => error.errors) 242 | const errorResponse = { errors: allErrors } 243 | const baseRes = res as unknown as Response 244 | baseRes.status(400).json(errorResponse) 245 | return 246 | } 247 | 248 | if ('useResponse' in schemas && schemas.useResponse) { 249 | const typedResponse = schemas.useResponse 250 | const originalStatus = res.status.bind(res) 251 | const originalJson = res.json.bind(res) 252 | const originalSend = res.send.bind(res) 253 | 254 | type ChainedResponse = Omit, 'status'> & { 255 | json(body: TypedJsonResponse): void 256 | send(body: TypedJsonResponse): void 257 | } 258 | 259 | const createChainedResponse = (code: StatusCodes): ChainedResponse => { 260 | originalStatus(code) 261 | const chainedRes = res as ChainedResponse 262 | chainedRes.json = (body: unknown) => { 263 | const schema = typedResponse[code] 264 | if (!schema) { 265 | throw new Error(`No schema defined for status code ${code}`) 266 | } 267 | const result = schema.parse(body) 268 | return originalJson(result) 269 | } 270 | chainedRes.send = (body: unknown) => { 271 | if (res.get('Content-Type')?.includes('application/json')) { 272 | body = JSON.parse(body as string) 273 | } 274 | 275 | const schema = typedResponse[code] 276 | if (!schema) { 277 | throw new Error(`No schema defined for status code ${code}`) 278 | } 279 | const result = schema.parse(body) 280 | 281 | if (res.get('Content-Type')?.includes('application/json')) { 282 | return originalSend(JSON.stringify(result) as TypedJsonResponse) 283 | } 284 | 285 | return originalSend(result) 286 | } 287 | return chainedRes 288 | } 289 | 290 | res.status = ((code: StatusCodes) => createChainedResponse(code)) as ValidatedResponse['status'] 291 | 292 | res.json = ((body: unknown) => { 293 | const schema = typedResponse[200] 294 | if (!schema) { 295 | throw new Error('No schema defined for status code 200') 296 | } 297 | const result = schema.parse(body) 298 | return originalJson(result) 299 | }) as ValidatedResponse['json'] 300 | 301 | res.send = ((body: unknown) => { 302 | if (res.get('Content-Type')?.includes('application/json')) { 303 | body = JSON.parse(body as string) 304 | } 305 | 306 | const schema = typedResponse[200] 307 | if (!schema) { 308 | throw new Error('No schema defined for status code 200') 309 | } 310 | 311 | const result = schema.parse(body) 312 | 313 | if (res.get('Content-Type')?.includes('application/json')) { 314 | return originalSend(JSON.stringify(result) as TypedJsonResponse) 315 | } 316 | 317 | return originalSend(result) 318 | }) as ValidatedResponse['send'] 319 | } 320 | 321 | next() 322 | } catch (error) { 323 | if (error instanceof ZodError) { 324 | const errorResponse = { errors: error.errors } 325 | const baseRes = res as unknown as Response 326 | baseRes.status(400).json(errorResponse) 327 | return 328 | } 329 | next(error) 330 | } 331 | } 332 | 333 | const wrappedMiddleware = ((baseReq: Request, baseRes: Response, next: NextFunction) => { 334 | if ('useResponse' in schemas && schemas.useResponse) { 335 | throw new Error('When using useResponse, you must call .use() to provide a handler') 336 | } 337 | const req = baseReq as ValidatedRequest 338 | const res = baseRes as unknown as ValidatedResponse 339 | return middleware(req, res, next) 340 | }) as ValidatedMiddleware 341 | 342 | return Object.assign(wrappedMiddleware, { 343 | schemas, 344 | handler: wrappedMiddleware, 345 | use: < 346 | P extends (req: ValidatedRequest, res: ValidatedResponse, next: NextFunction) => void, 347 | >( 348 | handler: P 349 | ) => { 350 | const handlerWrapper = ((baseReq: Request, baseRes: Response, next: NextFunction) => { 351 | const req = baseReq as ValidatedRequest 352 | const res = baseRes as unknown as ValidatedResponse 353 | 354 | middleware(req, res, (error?: unknown) => { 355 | if (error) { 356 | next(error) 357 | return 358 | } 359 | return handler(req, res, next) 360 | }) 361 | }) as RequestHandler 362 | 363 | return Object.assign(handlerWrapper, { 364 | schemas, 365 | }) 366 | }, 367 | }) as unknown as T extends TypedResponseValidationSchema 368 | ? Omit, 'handler' | 'schemas'> 369 | : ValidatedMiddleware 370 | } 371 | 372 | export { generateOpenApiSpec } from './generate-openapi-spec' 373 | -------------------------------------------------------------------------------- /tests/generate-openapi-spec.test.ts: -------------------------------------------------------------------------------- 1 | import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi' 2 | import express from 'express' 3 | import { type OpenAPIV3 } from 'openapi-types' 4 | import { z } from 'zod' 5 | import { 6 | JsonSchema7ObjectType, 7 | JsonSchema7UndefinedType, 8 | zodToJsonSchema, 9 | } from 'zod-to-json-schema' 10 | import { generateOpenApiSpec, validate } from '../src' 11 | 12 | extendZodWithOpenApi(z) 13 | 14 | describe('generateOpenApiSpec', () => { 15 | const app = express() 16 | app.use(express.json()) 17 | 18 | const bodySchema = z.object({ 19 | name: z.string(), 20 | age: z.number(), 21 | }) 22 | 23 | const paramsSchema = z.object({ 24 | id: z.string(), 25 | }) 26 | 27 | const querySchema = z.object({ 28 | filter: z.string().optional(), 29 | }) 30 | 31 | const headersSchema = z.object({ 32 | 'api-key': z.string(), 33 | }) 34 | 35 | const basicSchema = { 36 | params: paramsSchema, 37 | query: querySchema, 38 | headers: headersSchema, 39 | body: bodySchema, 40 | response: z.object({ 41 | id: z.string(), 42 | message: z.string(), 43 | }), 44 | } 45 | 46 | const advancedSchema = { 47 | params: paramsSchema, 48 | query: querySchema, 49 | headers: headersSchema, 50 | body: bodySchema, 51 | useResponse: { 52 | 200: z.object({ 53 | id: z.string(), 54 | message: z.string(), 55 | }), 56 | 204: z.never(), 57 | 400: z.object({ 58 | errors: z.array( 59 | z.object({ 60 | path: z.string(), 61 | message: z.string(), 62 | }) 63 | ), 64 | }), 65 | 500: z.object({ 66 | error: z.string(), 67 | }), 68 | }, 69 | } 70 | 71 | const basicPathName = '/test-basic/:id' 72 | const advancedPathName = '/separate/test-advanced/:id' 73 | 74 | app.post(basicPathName, validate(basicSchema), (_req, res) => { 75 | res.json({ 76 | id: '123', 77 | message: 'Hello World', 78 | }) 79 | }) 80 | 81 | // creating a separate router to also test generating docs for nested routers 82 | const separateRouter = express.Router() 83 | separateRouter.post( 84 | '/test-advanced/:id', 85 | validate(advancedSchema).use((_req, res) => { 86 | res.json({ 87 | id: '123', 88 | message: 'Hello World', 89 | }) 90 | }) 91 | ) 92 | 93 | // attach the separate router to the app 94 | app.use('/separate', separateRouter) 95 | 96 | // generate the OpenAPI Specification 97 | const spec = generateOpenApiSpec(app) 98 | 99 | it('should use the default info object for the OpenAPI Specification', async () => { 100 | expect(spec.openapi).toBe('3.0.0') 101 | expect(spec.info.version).toBe('1.0.0') 102 | expect(spec.info.title).toBe('API') 103 | expect(spec.info.description).toBe('Auto Generated API by echt') 104 | }) 105 | 106 | it('should use the default info object for the OpenAPI Specification', async () => { 107 | const customSpec = generateOpenApiSpec(app, { 108 | openapi: '3.0.0', 109 | info: { 110 | version: '2.0.0', 111 | title: 'Custom API', 112 | description: 'Custom API by echt', 113 | }, 114 | servers: [{ url: 'v1' }], 115 | }) 116 | 117 | expect(customSpec.openapi).toBe('3.0.0') 118 | expect(customSpec.info.version).toBe('2.0.0') 119 | expect(customSpec.info.title).toBe('Custom API') 120 | expect(customSpec.info.description).toBe('Custom API by echt') 121 | expect(customSpec.servers).toHaveLength(1) 122 | expect(customSpec.servers?.[0].url).toBe('v1') 123 | }) 124 | 125 | it('should include the request body in the OpenAPI Specification', async () => { 126 | const basicSchemaSpecRequestBody = spec.paths[basicPathName].post 127 | ?.requestBody as OpenAPIV3.RequestBodyObject 128 | 129 | if (!basicSchemaSpecRequestBody) { 130 | throw new Error('Basic schema spec is undefined') 131 | } 132 | 133 | const basicSchemaSpecRequestBodySchema = basicSchemaSpecRequestBody.content['application/json'] 134 | .schema as OpenAPIV3.SchemaObject 135 | 136 | const requestBodySchema = zodToJsonSchema(basicSchema.body) 137 | 138 | expect(requestBodySchema).toMatchObject(basicSchemaSpecRequestBodySchema) 139 | }) 140 | 141 | it('should include the response in the OpenAPI Specification', async () => { 142 | const basicSchemaSpecResponse = spec.paths[basicPathName].post?.responses['200'].content[ 143 | 'application/json' 144 | ].schema as OpenAPIV3.SchemaObject 145 | 146 | const responseSchema = zodToJsonSchema(basicSchema.response) 147 | 148 | expect(responseSchema).toMatchObject(basicSchemaSpecResponse) 149 | }) 150 | 151 | it('should include the params in the OpenAPI Specification', async () => { 152 | const basicSchemaSpecParams = spec.paths[basicPathName].post 153 | ?.parameters as OpenAPIV3.ParameterObject[] 154 | 155 | if (!basicSchemaSpecParams) { 156 | throw new Error('Basic schema spec params is undefined') 157 | } 158 | 159 | const pathParams = basicSchemaSpecParams.filter((param) => param.in === 'path') 160 | const paramsSchema = zodToJsonSchema(basicSchema.params) as JsonSchema7ObjectType 161 | 162 | expect(pathParams).toHaveLength(Object.keys(paramsSchema.properties).length) 163 | 164 | pathParams.forEach((param_) => { 165 | const param = param_ as OpenAPIV3.ParameterObject 166 | 167 | expect(param.schema).toMatchObject(paramsSchema.properties[param.name]) 168 | }) 169 | }) 170 | 171 | it('should include the query params in the OpenAPI Specification', async () => { 172 | const basicSchemaSpecQuery = spec.paths[basicPathName].post 173 | ?.parameters as OpenAPIV3.ParameterObject[] 174 | 175 | if (!basicSchemaSpecQuery) { 176 | throw new Error('Basic schema spec params is undefined') 177 | } 178 | 179 | const queryParams = basicSchemaSpecQuery.filter((param) => param.in === 'query') 180 | const querySchema = zodToJsonSchema(basicSchema.query) as JsonSchema7ObjectType 181 | 182 | expect(queryParams).toHaveLength(Object.keys(querySchema.properties).length) 183 | 184 | queryParams.forEach((param) => { 185 | expect(param.schema).toMatchObject(querySchema.properties[param.name]) 186 | }) 187 | }) 188 | 189 | it('should include the headers in the OpenAPI Specification', async () => { 190 | const basicSchemaSpecHeaders = spec.paths[basicPathName].post 191 | ?.parameters as OpenAPIV3.ParameterObject[] 192 | 193 | if (!basicSchemaSpecHeaders) { 194 | throw new Error('Basic schema spec params is undefined') 195 | } 196 | 197 | const headers = basicSchemaSpecHeaders.filter((param) => param.in === 'header') 198 | const headersSchema = zodToJsonSchema(basicSchema.headers) as JsonSchema7ObjectType 199 | 200 | expect(headers).toHaveLength(Object.keys(headersSchema.properties).length) 201 | 202 | headers.forEach((header) => { 203 | expect(header.schema).toMatchObject(headersSchema.properties[header.name]) 204 | }) 205 | }) 206 | 207 | it('should include the advanced schema response in the OpenAPI Specification', async () => { 208 | const advancedSchemaResponses = spec.paths[advancedPathName].post 209 | ?.responses as OpenAPIV3.ResponsesObject 210 | 211 | if (!advancedSchemaResponses) { 212 | throw new Error('Advanced schema responses is undefined') 213 | } 214 | 215 | for (const [status, schema_] of Object.entries(advancedSchemaResponses)) { 216 | const schema = schema_ as OpenAPIV3.ResponseObject 217 | const statusCode = Number.parseInt(status) as keyof typeof advancedSchema.useResponse 218 | const responseSchema = zodToJsonSchema(advancedSchema.useResponse[statusCode]) 219 | 220 | if ((responseSchema as JsonSchema7UndefinedType).not) { 221 | expect(schema.content).toBeUndefined() 222 | continue 223 | } 224 | 225 | const specResponseSchema = schema.content?.['application/json'] 226 | ?.schema as OpenAPIV3.SchemaObject 227 | 228 | expect(responseSchema).toMatchObject(specResponseSchema) 229 | } 230 | }) 231 | }) 232 | -------------------------------------------------------------------------------- /tests/validate.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import express from 'express' 3 | import { validate } from '../src' 4 | import { z } from 'zod' 5 | 6 | describe('validate middleware', () => { 7 | const app = express() 8 | app.use(express.json()) 9 | 10 | const schema = { 11 | body: z.object({ 12 | name: z.string().min(3), 13 | age: z.number().min(18) 14 | }), 15 | headers: z.object({ 16 | 'api-key': z.string(), 17 | accept: z.string(), 18 | 'set-cookie': z.string() 19 | }), 20 | query: z.object({ 21 | filter: z.string().optional() 22 | }) 23 | } 24 | 25 | const paramsSchema = { 26 | params: z.object({ 27 | id: z.string().regex(/^\d+$/) 28 | }) 29 | } 30 | 31 | const localsSchema = { 32 | locals: z.object({ 33 | userId: z.string() 34 | }) 35 | } 36 | 37 | const multiValidationSchema = { 38 | body: z.object({ 39 | name: z.string().min(3), 40 | email: z.string().email() 41 | }), 42 | query: z.object({ 43 | page: z.string() 44 | }), 45 | headers: z.object({ 46 | 'api-key': z.string() 47 | }) 48 | } 49 | 50 | const emptySchema = { 51 | body: z.object({}).strict() 52 | } 53 | 54 | // Mock error handler middleware 55 | const errorHandler = jest.fn((_err, _req, res, _next) => { 56 | res.status(500).json({ error: 'Internal server error' }) 57 | }) 58 | 59 | app.post('/test', validate(schema), (req, res) => { 60 | res.json(req.body) 61 | }) 62 | 63 | // Add error handler middleware 64 | app.use(errorHandler) 65 | 66 | beforeEach(() => { 67 | errorHandler.mockClear() 68 | }) 69 | 70 | it('should pass validation with valid data', async () => { 71 | const response = await request(app) 72 | .post('/test') 73 | .set('api-key', 'test-key') 74 | .set('accept', 'application/json') 75 | .set('set-cookie', 'session=123; user=john') 76 | .send({ 77 | name: 'John', 78 | age: 25 79 | }) 80 | 81 | expect(response.status).toBe(200) 82 | expect(response.body).toEqual({ 83 | name: 'John', 84 | age: 25 85 | }) 86 | }) 87 | 88 | it('should fail validation with invalid body', async () => { 89 | const response = await request(app).post('/test').set('api-key', 'test-key').send({ 90 | name: 'Jo', 91 | age: 15 92 | }) 93 | 94 | expect(response.status).toBe(400) 95 | expect(response.body).toHaveProperty('errors') 96 | }) 97 | 98 | it('should fail validation with missing headers', async () => { 99 | const response = await request(app).post('/test').send({ 100 | name: 'John', 101 | age: 25 102 | }) 103 | 104 | expect(response.status).toBe(400) 105 | expect(response.body).toHaveProperty('errors') 106 | }) 107 | 108 | it('should pass non-Zod errors to next middleware', async () => { 109 | const appWithError = express() 110 | appWithError.use(express.json()) 111 | 112 | // Create a schema that will throw a non-Zod error 113 | const errorSchema = { 114 | body: z.object({}).transform(() => { 115 | throw new Error('Unexpected error') 116 | }) 117 | } 118 | 119 | appWithError.post('/error', validate(errorSchema), (req, res) => { 120 | res.json(req.body) 121 | }) 122 | 123 | appWithError.use(errorHandler) 124 | 125 | const response = await request(appWithError).post('/error').send({ test: 'data' }) 126 | 127 | expect(response.status).toBe(500) 128 | expect(errorHandler).toHaveBeenCalledTimes(1) 129 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 130 | expect(errorHandler.mock.calls[0][0].message).toBe('Unexpected error') 131 | }) 132 | 133 | it('should handle null header values', async () => { 134 | const appWithNullHeader = express() 135 | appWithNullHeader.use(express.json()) 136 | 137 | // Middleware to inject a null header value 138 | appWithNullHeader.use((req, _res, next) => { 139 | req.headers['x-test'] = undefined 140 | next() 141 | }) 142 | 143 | const nullHeaderSchema = { 144 | headers: z.object({ 145 | 'x-test': z.string().optional() 146 | }) 147 | } 148 | 149 | appWithNullHeader.post('/null-header', validate(nullHeaderSchema), (req, res) => { 150 | res.json({ headers: req.headers }) 151 | }) 152 | 153 | const response = await request(appWithNullHeader).post('/null-header') 154 | 155 | expect(response.status).toBe(200) 156 | }) 157 | 158 | it('should validate URL parameters', async () => { 159 | const paramApp = express() 160 | paramApp.get('/:id', validate(paramsSchema), (req, res) => { 161 | res.json({ id: req.params.id }) 162 | }) 163 | 164 | const response = await request(paramApp).get('/123') 165 | expect(response.status).toBe(200) 166 | expect(response.body).toEqual({ id: '123' }) 167 | 168 | const invalidResponse = await request(paramApp).get('/abc') 169 | expect(invalidResponse.status).toBe(400) 170 | expect(invalidResponse.body).toHaveProperty('errors') 171 | }) 172 | 173 | it('should validate res.locals', async () => { 174 | const localsApp = express() 175 | localsApp.use((req, res, next) => { 176 | res.locals.userId = '123' 177 | next() 178 | }) 179 | 180 | localsApp.get('/', validate(localsSchema), (req, res) => { 181 | res.json({ userId: res.locals.userId }) 182 | }) 183 | 184 | const response = await request(localsApp).get('/') 185 | expect(response.status).toBe(200) 186 | expect(response.body).toEqual({ userId: '123' }) 187 | }) 188 | 189 | it('should return multiple validation errors', async () => { 190 | const multiApp = express() 191 | multiApp.use(express.json()) 192 | 193 | multiApp.post('/multi', validate(multiValidationSchema), (req, res) => { 194 | res.json(req.body) 195 | }) 196 | 197 | const response = await request(multiApp).post('/multi').send({ 198 | name: 'Jo', 199 | email: 'invalid-email' 200 | }) 201 | 202 | expect(response.status).toBe(400) 203 | expect(response.body.errors).toHaveLength(4) 204 | expect(response.body.errors.some((e: any) => e.path.includes('name'))).toBe(true) 205 | expect(response.body.errors.some((e: any) => e.path.includes('email'))).toBe(true) 206 | expect(response.body.errors.some((e: any) => e.path.includes('api-key'))).toBe(true) 207 | expect(response.body.errors.some((e: any) => e.path.includes('page'))).toBe(true) 208 | }) 209 | 210 | it('should validate empty objects', async () => { 211 | const emptyApp = express() 212 | emptyApp.use(express.json()) 213 | 214 | emptyApp.post('/empty', validate(emptySchema), (req, res) => { 215 | res.json({}) 216 | }) 217 | 218 | const response = await request(emptyApp).post('/empty').send({}) 219 | 220 | expect(response.status).toBe(200) 221 | 222 | const invalidResponse = await request(emptyApp).post('/empty').send({ extraField: 'not allowed' }) 223 | 224 | expect(invalidResponse.status).toBe(400) 225 | expect(invalidResponse.body).toHaveProperty('errors') 226 | }) 227 | 228 | it('should validate response with typed responses', async () => { 229 | const responseApp = express() 230 | responseApp.use(express.json()) 231 | 232 | const responseSchema = { 233 | useResponse: { 234 | 200: z.object({ 235 | message: z.string(), 236 | data: z.object({ id: z.number() }) 237 | }), 238 | 400: z.object({ 239 | error: z.string() 240 | }) 241 | } 242 | } 243 | 244 | responseApp.post( 245 | '/response', 246 | validate(responseSchema).use((req, res) => { 247 | res.json({ message: 'Success', data: { id: 123 } }) 248 | }) 249 | ) 250 | 251 | let response: any 252 | 253 | try { 254 | response = await request(responseApp).post('/response').send({}) 255 | } catch (error) {} 256 | 257 | expect(response.status).toBe(200) 258 | expect(response.body).toEqual({ 259 | message: 'Success', 260 | data: { id: 123 } 261 | }) 262 | 263 | // Test different status code 264 | responseApp.post( 265 | '/response-400', 266 | validate(responseSchema).use((req, res) => { 267 | res.status(400).json({ error: 'Bad request' }) 268 | }) 269 | ) 270 | 271 | const errorResponse = await request(responseApp).post('/response-400').send({}) 272 | 273 | expect(errorResponse.status).toBe(400) 274 | expect(errorResponse.body).toEqual({ 275 | error: 'Bad request' 276 | }) 277 | }) 278 | 279 | it('should throw error when response schema is not defined for status code', async () => { 280 | const responseApp = express() 281 | responseApp.use(express.json()) 282 | 283 | const responseSchema = { 284 | useResponse: { 285 | 200: z.object({ 286 | message: z.string() 287 | }) 288 | } 289 | } 290 | 291 | responseApp.post( 292 | '/invalid-status', 293 | validate(responseSchema).use((req, res) => { 294 | res.status(201).json({ message: 'Created' }) 295 | }) 296 | ) 297 | 298 | responseApp.use(errorHandler) 299 | 300 | const response = await request(responseApp).post('/invalid-status').send({}) 301 | 302 | expect(response.status).toBe(500) 303 | expect(errorHandler).toHaveBeenCalled() 304 | }) 305 | 306 | it('should throw error when using useResponse without .use()', async () => { 307 | const responseApp = express() 308 | responseApp.use(express.json()) 309 | 310 | const responseSchema = { 311 | useResponse: { 312 | 200: z.object({ 313 | message: z.string() 314 | }) 315 | } 316 | } 317 | 318 | responseApp.post('/invalid-usage', validate(responseSchema) as any, (req, res) => { 319 | res.json({ message: 'Success' }) 320 | }) 321 | 322 | responseApp.use(errorHandler) 323 | 324 | const response = await request(responseApp).post('/invalid-usage').send({}) 325 | 326 | expect(response.status).toBe(500) 327 | expect(errorHandler).toHaveBeenCalled() 328 | }) 329 | 330 | it('should validate response with res.send()', async () => { 331 | const responseApp = express() 332 | responseApp.use(express.json()) 333 | 334 | const responseSchema = { 335 | useResponse: { 336 | 200: z.object({ 337 | message: z.string() 338 | }) 339 | } 340 | } 341 | 342 | responseApp.post( 343 | '/send', 344 | validate(responseSchema).use((req, res) => { 345 | res.send({ message: 'Success' }) 346 | }) 347 | ) 348 | 349 | const response = await request(responseApp).post('/send').send({}) 350 | 351 | expect(response.status).toBe(200) 352 | expect(response.body).toEqual({ 353 | message: 'Success' 354 | }) 355 | }) 356 | 357 | it('should throw error when response validation fails', async () => { 358 | const responseApp = express() 359 | responseApp.use(express.json()) 360 | 361 | const responseSchema = { 362 | useResponse: { 363 | 200: z.object({ 364 | message: z.string(), 365 | count: z.number() 366 | }) 367 | } 368 | } 369 | 370 | responseApp.post( 371 | '/invalid-response', 372 | validate(responseSchema).use((req, res) => { 373 | // @ts-ignore - intentionally sending invalid response for testing 374 | res.json({ message: 'Success' }) // Missing required count field 375 | }) 376 | ) 377 | 378 | responseApp.use(errorHandler) 379 | 380 | const response = await request(responseApp).post('/invalid-response').send({}) 381 | 382 | expect(response.status).toBe(500) 383 | expect(errorHandler).toHaveBeenCalled() 384 | }) 385 | 386 | it('should pass non-Zod errors from headers validation to next middleware', async () => { 387 | const appWithError = express() 388 | appWithError.use(express.json()) 389 | 390 | const errorSchema = { 391 | headers: z.object({}).transform(() => { 392 | throw new Error('Headers transform error') 393 | }) 394 | } 395 | 396 | appWithError.post('/error-headers', validate(errorSchema), (req, res) => { 397 | res.json({}) 398 | }) 399 | 400 | appWithError.use(errorHandler) 401 | 402 | const response = await request(appWithError).post('/error-headers').send({}) 403 | 404 | expect(response.status).toBe(500) 405 | expect(errorHandler).toHaveBeenCalledTimes(1) 406 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 407 | expect(errorHandler.mock.calls[0][0].message).toBe('Headers transform error') 408 | }) 409 | 410 | it('should pass non-Zod errors from query validation to next middleware', async () => { 411 | const appWithError = express() 412 | appWithError.use(express.json()) 413 | 414 | const errorSchema = { 415 | query: z.object({}).transform(() => { 416 | throw new Error('Query transform error') 417 | }) 418 | } 419 | 420 | appWithError.post('/error-query', validate(errorSchema), (req, res) => { 421 | res.json({}) 422 | }) 423 | 424 | appWithError.use(errorHandler) 425 | 426 | const response = await request(appWithError).post('/error-query').send({}) 427 | 428 | expect(response.status).toBe(500) 429 | expect(errorHandler).toHaveBeenCalledTimes(1) 430 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 431 | expect(errorHandler.mock.calls[0][0].message).toBe('Query transform error') 432 | }) 433 | 434 | it('should pass non-Zod errors from params validation to next middleware', async () => { 435 | const appWithError = express() 436 | appWithError.use(express.json()) 437 | 438 | const errorSchema = { 439 | params: z.object({}).transform(() => { 440 | throw new Error('Params transform error') 441 | }) 442 | } 443 | 444 | appWithError.post('/error-params', validate(errorSchema), (req, res) => { 445 | res.json({}) 446 | }) 447 | 448 | appWithError.use(errorHandler) 449 | 450 | const response = await request(appWithError).post('/error-params').send({}) 451 | 452 | expect(response.status).toBe(500) 453 | expect(errorHandler).toHaveBeenCalledTimes(1) 454 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 455 | expect(errorHandler.mock.calls[0][0].message).toBe('Params transform error') 456 | }) 457 | 458 | it('should pass non-Zod errors from locals validation to next middleware', async () => { 459 | const appWithError = express() 460 | appWithError.use(express.json()) 461 | 462 | const errorSchema = { 463 | locals: z.object({}).transform(() => { 464 | throw new Error('Locals transform error') 465 | }) 466 | } 467 | 468 | appWithError.use((req, res, next) => { 469 | res.locals = {} 470 | next() 471 | }) 472 | 473 | appWithError.post('/error-locals', validate(errorSchema), (req, res) => { 474 | res.json({}) 475 | }) 476 | 477 | appWithError.use(errorHandler) 478 | 479 | const response = await request(appWithError).post('/error-locals').send({}) 480 | 481 | expect(response.status).toBe(500) 482 | expect(errorHandler).toHaveBeenCalledTimes(1) 483 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 484 | expect(errorHandler.mock.calls[0][0].message).toBe('Locals transform error') 485 | }) 486 | 487 | it('should pass non-Zod errors from response validation to next middleware', async () => { 488 | const appWithError = express() 489 | appWithError.use(express.json()) 490 | 491 | const errorSchema = { 492 | useResponse: { 493 | 200: z 494 | .object({}) 495 | .passthrough() 496 | .transform(() => { 497 | throw new Error('Response transform error') 498 | }) 499 | } 500 | } 501 | 502 | appWithError.post( 503 | '/error-response', 504 | validate(errorSchema).use((req, res) => { 505 | // @ts-ignore - intentionally sending invalid response for testing 506 | res.json({}) 507 | }) 508 | ) 509 | 510 | appWithError.use(errorHandler) 511 | 512 | const response = await request(appWithError).post('/error-response').send({}) 513 | 514 | expect(response.status).toBe(500) 515 | expect(errorHandler).toHaveBeenCalledTimes(1) 516 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 517 | expect(errorHandler.mock.calls[0][0].message).toBe('Response transform error') 518 | }) 519 | 520 | it('should validate locals with multiple fields', async () => { 521 | const localsApp = express() 522 | const complexLocalsSchema = { 523 | locals: z.object({ 524 | userId: z.string(), 525 | role: z.enum(['admin', 'user']), 526 | permissions: z.array(z.string()), 527 | metadata: z.object({ 528 | lastAccess: z.string() 529 | }) 530 | }) 531 | } 532 | 533 | localsApp.use((req, res, next) => { 534 | res.locals = { 535 | userId: '123', 536 | role: 'admin', 537 | permissions: ['read', 'write'], 538 | metadata: { 539 | lastAccess: '2024-01-01' 540 | } 541 | } 542 | next() 543 | }) 544 | 545 | localsApp.get('/', validate(complexLocalsSchema), (req, res) => { 546 | res.json(res.locals) 547 | }) 548 | 549 | const response = await request(localsApp).get('/') 550 | expect(response.status).toBe(200) 551 | expect(response.body).toEqual({ 552 | userId: '123', 553 | role: 'admin', 554 | permissions: ['read', 'write'], 555 | metadata: { 556 | lastAccess: '2024-01-01' 557 | } 558 | }) 559 | }) 560 | 561 | it('should fail validation with invalid locals data', async () => { 562 | const localsApp = express() 563 | const localsSchema = { 564 | locals: z.object({ 565 | userId: z.string(), 566 | count: z.number().positive() 567 | }) 568 | } 569 | 570 | localsApp.use((req, res, next) => { 571 | res.locals = { 572 | userId: '123', 573 | count: -1 // Invalid: should be positive 574 | } 575 | next() 576 | }) 577 | 578 | localsApp.get('/', validate(localsSchema), (req, res) => { 579 | res.json(res.locals) 580 | }) 581 | 582 | const response = await request(localsApp).get('/') 583 | expect(response.status).toBe(400) 584 | expect(response.body).toHaveProperty('errors') 585 | expect(response.body.errors[0].path).toContain('count') 586 | }) 587 | 588 | it('should handle undefined locals with optional fields', async () => { 589 | const localsApp = express() 590 | const optionalLocalsSchema = { 591 | locals: z.object({ 592 | userId: z.string(), 593 | sessionData: z 594 | .object({ 595 | lastLogin: z.string() 596 | }) 597 | .optional(), 598 | preferences: z.record(z.string()).optional() 599 | }) 600 | } 601 | 602 | localsApp.use((req, res, next) => { 603 | res.locals = { 604 | userId: '123' 605 | // sessionData and preferences are intentionally omitted 606 | } 607 | next() 608 | }) 609 | 610 | localsApp.get('/', validate(optionalLocalsSchema), (req, res) => { 611 | res.json(res.locals) 612 | }) 613 | 614 | const response = await request(localsApp).get('/') 615 | expect(response.status).toBe(200) 616 | expect(response.body).toEqual({ 617 | userId: '123' 618 | }) 619 | }) 620 | 621 | it('should handle non-JSON response content type', async () => { 622 | const responseApp = express() 623 | responseApp.use(express.json()) 624 | 625 | const responseSchema = { 626 | useResponse: { 627 | 200: z.string() 628 | } 629 | } 630 | 631 | responseApp.post( 632 | '/text', 633 | validate(responseSchema).use((req, res) => { 634 | res.type('text/plain') 635 | // @ts-ignore - intentionally sending string response for testing 636 | res.send('Hello World') 637 | }) 638 | ) 639 | 640 | const response = await request(responseApp).post('/text').send({}) 641 | expect(response.status).toBe(200) 642 | expect(response.text).toBe('Hello World') 643 | }) 644 | 645 | it('should handle JSON stringified response with content type', async () => { 646 | const responseApp = express() 647 | responseApp.use(express.json()) 648 | 649 | const responseSchema = { 650 | useResponse: { 651 | 200: z.object({ 652 | message: z.string() 653 | }) 654 | } 655 | } 656 | 657 | responseApp.post( 658 | '/json-string', 659 | validate(responseSchema).use((req, res) => { 660 | res.type('application/json') 661 | // @ts-ignore - intentionally sending stringified JSON for testing 662 | res.send(JSON.stringify({ message: 'Hello' })) 663 | }) 664 | ) 665 | 666 | const response = await request(responseApp).post('/json-string').send({}) 667 | expect(response.status).toBe(200) 668 | expect(response.body).toEqual({ message: 'Hello' }) 669 | }) 670 | 671 | it('should throw error when response schema is missing for status code', async () => { 672 | const responseApp = express() 673 | responseApp.use(express.json()) 674 | 675 | const responseSchema = { 676 | useResponse: { 677 | 201: z.object({ 678 | message: z.string() 679 | }) 680 | } 681 | } 682 | 683 | responseApp.post( 684 | '/missing-schema', 685 | validate(responseSchema).use((req, res) => { 686 | // Trying to send 200 response when only 201 is defined 687 | // @ts-ignore - intentionally sending response for undefined status code 688 | res.status(200).send({ message: 'Success' }) 689 | }) 690 | ) 691 | 692 | responseApp.use(errorHandler) 693 | 694 | const response = await request(responseApp).post('/missing-schema').send({}) 695 | 696 | expect(response.status).toBe(500) 697 | expect(errorHandler).toHaveBeenCalledTimes(1) 698 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 699 | expect(errorHandler.mock.calls[0][0].message).toBe('No schema defined for status code 200') 700 | }) 701 | 702 | it('should successfully validate response when schema matches status code', async () => { 703 | const responseApp = express() 704 | responseApp.use(express.json()) 705 | 706 | const responseSchema = { 707 | useResponse: { 708 | 200: z.object({ 709 | message: z.string() 710 | }) 711 | } 712 | } 713 | 714 | responseApp.post( 715 | '/missing-schema', 716 | validate(responseSchema).use((req, res) => { 717 | res.status(200).send({ message: 'Success' }) 718 | }) 719 | ) 720 | 721 | responseApp.use(errorHandler) 722 | 723 | const response = await request(responseApp).post('/missing-schema').send({}) 724 | 725 | expect(response.status).toBe(200) 726 | }) 727 | 728 | it('should handle invalid JSON in response with content type', async () => { 729 | const responseApp = express() 730 | responseApp.use(express.json()) 731 | 732 | const responseSchema = { 733 | useResponse: { 734 | 200: z.object({ 735 | message: z.string() 736 | }) 737 | } 738 | } 739 | 740 | responseApp.post( 741 | '/invalid-json', 742 | validate(responseSchema).use((req, res) => { 743 | res.type('application/json') 744 | // @ts-ignore - intentionally sending invalid JSON for testing 745 | res.send('{"message": "Hello"') // Invalid JSON - missing closing brace 746 | }) 747 | ) 748 | 749 | responseApp.use(errorHandler) 750 | 751 | const response = await request(responseApp).post('/invalid-json').send({}) 752 | expect(response.status).toBe(500) 753 | expect(errorHandler).toHaveBeenCalledTimes(1) 754 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 755 | expect(errorHandler.mock.calls[0][0].message).toContain("Expected ',' or '}' after property value in JSON") 756 | }) 757 | 758 | it('should handle JSON response with status code and content type', async () => { 759 | const responseApp = express() 760 | responseApp.use(express.json()) 761 | 762 | const responseSchema = { 763 | useResponse: { 764 | 201: z.object({ 765 | message: z.string() 766 | }) 767 | } 768 | } 769 | 770 | responseApp.post( 771 | '/json-with-status', 772 | validate(responseSchema).use((req, res) => { 773 | res.type('application/json') 774 | 775 | // @ts-ignore - intentionally sending stringified JSON for testing 776 | res.status(201).send(JSON.stringify({ message: 'Created' })) 777 | }) 778 | ) 779 | 780 | const response = await request(responseApp).post('/json-with-status').send({}) 781 | expect(response.status).toBe(201) 782 | expect(response.body).toEqual({ message: 'Created' }) 783 | }) 784 | 785 | it('should throw error when response schema is missing for res.json', async () => { 786 | const responseApp = express() 787 | responseApp.use(express.json()) 788 | 789 | const responseSchema = { 790 | useResponse: { 791 | 201: z.object({ 792 | message: z.string() 793 | }) 794 | // 200 is intentionally not defined 795 | } 796 | } 797 | 798 | responseApp.post( 799 | '/missing-schema-json', 800 | validate(responseSchema).use((req, res) => { 801 | // @ts-ignore - intentionally using undefined status code 802 | res.json({ message: 'Success' }) // This will use default 200 status code 803 | }) 804 | ) 805 | 806 | responseApp.use(errorHandler) 807 | 808 | const response = await request(responseApp).post('/missing-schema-json').send({}) 809 | 810 | expect(response.status).toBe(500) 811 | expect(errorHandler).toHaveBeenCalledTimes(1) 812 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 813 | expect(errorHandler.mock.calls[0][0].message).toBe('No schema defined for status code 200') 814 | }) 815 | 816 | it('should throw error when response schema is missing for res.send', async () => { 817 | const responseApp = express() 818 | responseApp.use(express.json()) 819 | 820 | const responseSchema = { 821 | useResponse: { 822 | 201: z.object({ 823 | message: z.string() 824 | }) 825 | // 200 is intentionally not defined 826 | } 827 | } 828 | 829 | responseApp.post( 830 | '/missing-schema-send', 831 | validate(responseSchema).use((req, res) => { 832 | // @ts-ignore - intentionally using undefined status code 833 | res.send({ message: 'Success' }) // This will use default 200 status code 834 | }) 835 | ) 836 | 837 | responseApp.use(errorHandler) 838 | 839 | const response = await request(responseApp).post('/missing-schema-send').send({}) 840 | 841 | expect(response.status).toBe(500) 842 | expect(errorHandler).toHaveBeenCalledTimes(1) 843 | expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error) 844 | expect(errorHandler.mock.calls[0][0].message).toBe('No schema defined for status code 200') 845 | }) 846 | }) 847 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": [ 13 | "src" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "tests", 18 | "dist" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------