├── example ├── typescript │ ├── tsconfig.tsbuildinfo │ ├── tsconfig.json │ ├── route.ts │ └── server.ts └── javascript │ ├── users.js │ ├── server.js │ └── router.js ├── .gitignore ├── loadtest ├── .editorconfig ├── .github └── workflows │ └── ci.yaml ├── LICENSE ├── CHANGELOG.md ├── package.json ├── express-joi-validation.js ├── express-joi-validation.d.ts ├── index.test.js └── README.md /example/typescript/tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./server.ts","./route.ts"],"version":"5.7.2"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | coverage 5 | .nyc_output 6 | package-lock.json 7 | example/typescript/*.js 8 | example/typescript/*.d.ts 9 | .idea 10 | -------------------------------------------------------------------------------- /loadtest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | x=1 3 | 4 | while [ $x -le 10 ] 5 | do 6 | ab -n 1000 -c 100 -q http://127.0.0.1:8080/users?name=dean | grep -i "Requests per second" 7 | x=$(( $x + 1 )) 8 | sleep 5 9 | done 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Tells the .editorconfg plugin to stop searching once it finds this file 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.py] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /example/javascript/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = [ 4 | { id: 1000, name: 'anne, a.', age: 25 }, 5 | { id: 1001, name: 'barry, a.', age: 52 }, 6 | { id: 1002, name: 'clare, a.', age: 25 }, 7 | { id: 1003, name: 'joe, a.', age: 67 }, 8 | { id: 1004, name: 'anne, b.', age: 47 }, 9 | { id: 1005, name: 'barry, b.', age: 80 }, 10 | { id: 1006, name: 'clare, b.', age: 28 }, 11 | { id: 1007, name: 'joe, b.', age: 15 } 12 | ] 13 | -------------------------------------------------------------------------------- /example/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "moduleResolution": "node", 11 | "sourceMap": false, 12 | "baseUrl": ".", 13 | "declaration": false 14 | }, 15 | "include": ["server.ts", "route.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | express-version: [4, 5] 17 | node-version: [18.x, 20.x, 22.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm install express@${{ matrix.express-version }} 28 | - run: npm test 29 | - name: Coveralls 30 | uses: coverallsapp/github-action@v2 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /example/javascript/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.title = 'express-joi-validation' 4 | 5 | const port = 8080 6 | 7 | const app = require('express')() 8 | const Joi = require('joi') 9 | const validator = require('../../').createValidator() 10 | 11 | const headerSchema = Joi.object({ 12 | host: Joi.string().required(), 13 | 'user-agent': Joi.string().required() 14 | }) 15 | 16 | app.use(validator.headers(headerSchema)) 17 | 18 | app.use('/users', require('./router')) 19 | 20 | app.listen(port, err => { 21 | if (err) { 22 | throw err 23 | } 24 | 25 | console.log(`\napp started on ${port}\n`) 26 | console.log( 27 | `Try accessing http://localhost:${port}/users/1001 or http://localhost:${port}/users?name=barry to get some data.\n` 28 | ) 29 | console.log( 30 | `Now try access http://localhost:${port}/users?age=50. You should get an error complaining that your querystring is invalid.` 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017-2025 Evan Shortiss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /example/typescript/route.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi' 2 | import formidable from 'express-formidable' 3 | import { 4 | ValidatedRequest, 5 | ValidatedRequestWithRawInputsAndFields, 6 | ValidatedRequestSchema, 7 | createValidator, 8 | ContainerTypes 9 | } from '../../express-joi-validation' 10 | import { Router } from 'express' 11 | 12 | const route = Router() 13 | const validator = createValidator() 14 | const schema = Joi.object({ 15 | name: Joi.string().required() 16 | }) 17 | 18 | interface HelloGetRequestSchema extends ValidatedRequestSchema { 19 | [ContainerTypes.Query]: { 20 | name: string 21 | } 22 | } 23 | 24 | interface HelloPostRequestSchema extends ValidatedRequestSchema { 25 | [ContainerTypes.Fields]: { 26 | name: string 27 | } 28 | } 29 | 30 | // curl http://localhost:3030/hello/?name=express 31 | route.get( 32 | '/', 33 | validator.query(schema), 34 | (req: ValidatedRequest, res) => { 35 | res.end(`Hello ${req.query.name}`) 36 | } 37 | ) 38 | 39 | // curl -X POST -F 'name=express' http://localhost:3030/hello 40 | route.post('/', formidable(), validator.fields(schema), (req, res) => { 41 | const vreq = (req as unknown) as ValidatedRequestWithRawInputsAndFields< 42 | HelloPostRequestSchema 43 | > 44 | 45 | res.end(`Hello ${vreq.fields.name}`) 46 | }) 47 | 48 | export default route 49 | -------------------------------------------------------------------------------- /example/typescript/server.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const port = 3030 4 | 5 | import express from 'express' 6 | import * as Joi from 'joi' 7 | import HelloWorld from './route' 8 | import { createValidator, ExpressJoiError } from '../../express-joi-validation' 9 | 10 | const app = express() 11 | const validator = createValidator() 12 | 13 | const headerSchema = Joi.object({ 14 | host: Joi.string().required(), 15 | 'user-agent': Joi.string().required() 16 | }) 17 | 18 | // Validate headers for all incoming requests 19 | app.use(validator.headers(headerSchema)) 20 | 21 | // No extra validations performed on this simple ping endpoint 22 | app.get('/ping', (req, res) => { 23 | res.end('pong') 24 | }) 25 | 26 | app.use('/hello', HelloWorld) 27 | 28 | // Custom error handler 29 | app.use( 30 | ( 31 | err: any | ExpressJoiError, 32 | req: express.Request, 33 | res: express.Response, 34 | next: express.NextFunction 35 | ) => { 36 | if (err && err.error && err.error.isJoi) { 37 | const e: ExpressJoiError = err 38 | // e.g "you submitted a bad query" 39 | res.status(400).end(`You submitted a bad ${e.type} paramater.`) 40 | } else { 41 | res.status(500).end('internal server error') 42 | } 43 | } 44 | ) 45 | 46 | app.listen(port, () => { 47 | console.log(`\napp started on ${port}\n`) 48 | console.log( 49 | `Try accessing http://localhost:${port}/ping or http://localhost:${port}/hello?name=dean to get some data.\n` 50 | ) 51 | console.log( 52 | `Now try access http://localhost:${port}/hello. You should get an error complaining that your querystring is invalid.` 53 | ) 54 | }) 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | Date format is DD/MM/YYYY 3 | 4 | ## 6.1.0 (27/05/2025) 5 | 6 | * Implement express v5 compatibility (#47) 7 | * Use Prettier instead of ESLint 8 | 9 | ## 6.0.0 (13/10/2024) 10 | * Support Node.js 18+ 11 | * Narrow type for error handlers (#46) 12 | 13 | ## 5.0.0 (13/10/2020) 14 | * Drop Node.js 8 support. 15 | * Update to use Joi v17.x. 16 | * Change from using peerDependency of "@hapi/joi" to "joi". 17 | 18 | ## 4.0.3 (18/11/2019) 19 | * Fix TypeScript example in the README. 20 | 21 | ## 4.0.2 (12/11/2019) 22 | * Apply a fix for compatibility with Joi v16 typings. 23 | 24 | ## 4.0.1 (24/09/2019) 25 | * Remove outdated "joi" option in README 26 | 27 | ## 4.0.0 (20/09/2019) 28 | * Update to support Joi v16.x 29 | * No longer supports passing a Joi instance to factory 30 | * Finally removed deprecated function on `module.exports` from v2 31 | 32 | ## 3.0.0 (30/08/2019) 33 | * Removed `fields`, `originalQuery`, `originalHeaders`, `originalBody`, 34 | `originalParams`, and `originalFields` from `ValidatedRequest`. This simplifies 35 | usage with TypeScript's *strict* mode. 36 | * Added `ValidatedRequestWithRawInputsAndFields`. This is the same as 37 | `ValidatedRequest` from versions 2.x. 38 | 39 | ## 2.0.1 (22/08/2019) 40 | * Fixed compilation issue with TypeScript example when `strict` compiler flag is `true`. 41 | * Updated test script to include building TypeScript example 42 | 43 | ## 2.0.0 (27/06/2019) 44 | * Improved TypeScript support with better typings 45 | * Changed export from a factory function to a module exposing `createValidator()` 46 | * Improved TypeScript examples and README 47 | 48 | ## 1.0.0 (13/06/2019) 49 | * Migrated from `joi` to `@hapi/joi`. 50 | * Dropped Node.js 6 & 7 support (@hapi/joi forces this) 51 | * Update dev dependencies. 52 | 53 | ## 0.3.0 (29/09/2018) 54 | * Add response validation 55 | * Update dependencies 56 | * Drop support for Node.js 4 and below 57 | * Remove @types/express from dependencies 58 | 59 | ## 0.2.1 (28/10/2017) 60 | * Ensure "typings" are defined in package.json 61 | 62 | ## 0.2.0 (28/10/2017) 63 | * Add TypeScript support 64 | * Add new `fields` function for use with express-formidable 65 | 66 | ## 0.1.0 (16/04/2017) 67 | * Initial release. 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-joi-validation", 3 | "version": "6.1.0", 4 | "description": "validate express application inputs and parameters using joi", 5 | "main": "express-joi-validation.js", 6 | "scripts": { 7 | "build-ts": "tsc --build example/typescript/tsconfig.json", 8 | "unit": "mocha *.test.js", 9 | "ts-test": "tsc express-joi-validation.d.ts --target es5 --module commonjs --noEmit", 10 | "test": "npm run ts-test && npm run cover && nyc check-coverage --statements 100 --lines 100 --functions 100 --branches 100 && npm run build-ts", 11 | "cover": "nyc --reporter=lcov --produce-source-map=true npm run unit", 12 | "example": "nodemon example/javascript/server.js", 13 | "example-ts": "npm run build-ts && node example/typescript/server.js", 14 | "coveralls": "npm run cover && cat coverage/lcov.info | coveralls" 15 | }, 16 | "typings": "express-joi-validation.d.ts", 17 | "files": [ 18 | "express-joi-validation.js", 19 | "express-joi-validation.d.ts" 20 | ], 21 | "keywords": [ 22 | "joi", 23 | "express", 24 | "validation", 25 | "middleware", 26 | "typescript", 27 | "tsc" 28 | ], 29 | "author": "Evan Shortiss", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@types/express": "~4.0.39", 33 | "@types/express-formidable": "~1.0.4", 34 | "@types/node": "^18.19.68", 35 | "@types/qs": "~6.9.3", 36 | "body-parser": "1.20.3", 37 | "chai": "~3.5.0", 38 | "clear-require": "~2.0.0", 39 | "coveralls": "~3.0.2", 40 | "express": "~4.17.3", 41 | "express-formidable": "~1.0.0", 42 | "husky": "~1.0.1", 43 | "joi": "~17.5.0", 44 | "joi-extract-type": "~15.0.8", 45 | "lint-staged": "~8.2.1", 46 | "lodash": "~4.17.15", 47 | "mocha": "~8.1.3", 48 | "mocha-lcov-reporter": "~1.3.0", 49 | "nodemon": "~2.0.4", 50 | "nyc": "~15.1.0", 51 | "prettier": "~1.14.3", 52 | "proxyquire": "~1.7.11", 53 | "qs": "~6.9.4", 54 | "sinon": "~1.17.7", 55 | "supertest": "~3.0.0", 56 | "typescript": "^5.7.2" 57 | }, 58 | "peerDependencies": { 59 | "joi": "17" 60 | }, 61 | "engines": { 62 | "node": ">=18.0.0" 63 | }, 64 | "directories": { 65 | "example": "example" 66 | }, 67 | "repository": { 68 | "type": "git", 69 | "url": "git+https://github.com/evanshortiss/express-joi-validation.git" 70 | }, 71 | "bugs": { 72 | "url": "https://github.com/evanshortiss/express-joi-validation/issues" 73 | }, 74 | "homepage": "https://github.com/evanshortiss/express-joi-validation#readme", 75 | "husky": { 76 | "hooks": { 77 | "pre-commit": "lint-staged" 78 | } 79 | }, 80 | "lint-staged": { 81 | "*.js": [ 82 | "prettier --write", 83 | "git add" 84 | ], 85 | "*.ts": [ 86 | "prettier --write", 87 | "git add" 88 | ] 89 | }, 90 | "prettier": { 91 | "semi": false, 92 | "singleQuote": true 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /example/javascript/router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const route = (module.exports = require('express').Router()) 4 | const users = require('./users') 5 | const Joi = require('joi') 6 | const _ = require('lodash') 7 | const validator = require('../../').createValidator() 8 | 9 | /** 10 | * This "GET /:id" endpoint is used to query users by their ID 11 | * Try accessing http://localhost:8080/users/1001 to see it in action. 12 | * Now try http://localhost:8080/users/bananas - it will fail since the ID must be an integer 13 | */ 14 | const paramsSchema = Joi.object({ 15 | id: Joi.number().required() 16 | }) 17 | 18 | route.get('/:id', validator.params(paramsSchema), (req, res) => { 19 | console.log(`\nGetting user by ID ${req.params.id}.`) 20 | console.log( 21 | `req.params was ${JSON.stringify(req.originalParams)} before validation` 22 | ) 23 | console.log(`req.params is ${JSON.stringify(req.params)} after validation`) 24 | console.log('note that the ID was correctly cast to an integer') 25 | 26 | const u = _.find(users, { id: req.params.id }) 27 | 28 | if (u) { 29 | res.json(u) 30 | } else { 31 | res.status(404).json({ 32 | message: `no user exists with id "${req.params.id}"` 33 | }) 34 | } 35 | }) 36 | 37 | /** 38 | * This "GET /" endpoint is used to query users by a querystring 39 | * Try accessing http://localhost:8080/users?name=j&age=25 to get users that are 25 or with a name containing a "j". 40 | * Now try http://localhost:8080/users - it will fail since name is required 41 | */ 42 | const querySchema = Joi.object({ 43 | name: Joi.string() 44 | .required() 45 | .min(1) 46 | .max(10), 47 | age: Joi.number() 48 | .integer() 49 | .min(1) 50 | .max(120) 51 | }) 52 | 53 | route.get('/', validator.query(querySchema), (req, res) => { 54 | console.log(`\nGetting users for query ${JSON.stringify(req.query)}.`) 55 | console.log( 56 | `req.query was ${JSON.stringify(req.originalQuery)} before validation` 57 | ) 58 | console.log(`req.query is ${JSON.stringify(req.query)} after validation`) 59 | console.log( 60 | 'note that the age was correctly cast to an integer if provided\n' 61 | ) 62 | 63 | res.json( 64 | _.filter(users, u => { 65 | return ( 66 | _.includes(u.name, req.query.name) || 67 | (req.query.age && u.age === req.query.age) 68 | ) 69 | }) 70 | ) 71 | }) 72 | 73 | /** 74 | * This "POST /" endpoint is used to create new users 75 | * POST to http://localhost:8080/users with '{"name": "jane", "age": "26"}' to see it work 76 | * Now try posting '{"name": "jane", "age": 1000}' - it will fail since the age is above 120 77 | */ 78 | const bodySchema = Joi.object({ 79 | name: Joi.string() 80 | .required() 81 | .min(1) 82 | .max(10), 83 | age: Joi.number() 84 | .integer() 85 | .required() 86 | .min(1) 87 | .max(120) 88 | }) 89 | 90 | route.post( 91 | '/', 92 | require('body-parser').json(), 93 | validator.body(bodySchema), 94 | (req, res) => { 95 | console.log(`\Creating user with data ${JSON.stringify(req.body)}.`) 96 | console.log( 97 | `req.body was ${JSON.stringify(req.originalBody)} before validation` 98 | ) 99 | console.log(`req.body is ${JSON.stringify(req.body)} after validation`) 100 | console.log( 101 | 'note that the age was correctly cast to an integer if it was a string\n' 102 | ) 103 | 104 | // Generate data required for insert (new id is incremented from previous max) 105 | const prevMaxId = _.maxBy(users, u => u.id).id 106 | const data = Object.assign({}, req.body, { id: prevMaxId + 1 }) 107 | 108 | users.push(data) 109 | 110 | res.json({ message: 'created user', data: data }) 111 | } 112 | ) 113 | -------------------------------------------------------------------------------- /express-joi-validation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // These represent the incoming data containers that we might need to validate 4 | const containers = { 5 | query: { 6 | storageProperty: 'originalQuery', 7 | joi: { 8 | convert: true, 9 | allowUnknown: false, 10 | abortEarly: false 11 | } 12 | }, 13 | // For use with body-parser 14 | body: { 15 | storageProperty: 'originalBody', 16 | joi: { 17 | convert: true, 18 | allowUnknown: false, 19 | abortEarly: false 20 | } 21 | }, 22 | headers: { 23 | storageProperty: 'originalHeaders', 24 | joi: { 25 | convert: true, 26 | allowUnknown: true, 27 | stripUnknown: false, 28 | abortEarly: false 29 | } 30 | }, 31 | // URL params e.g "/users/:userId" 32 | params: { 33 | storageProperty: 'originalParams', 34 | joi: { 35 | convert: true, 36 | allowUnknown: false, 37 | abortEarly: false 38 | } 39 | }, 40 | // For use with express-formidable or similar POST body parser for forms 41 | fields: { 42 | storageProperty: 'originalFields', 43 | joi: { 44 | convert: true, 45 | allowUnknown: false, 46 | abortEarly: false 47 | } 48 | } 49 | } 50 | 51 | function buildErrorString(err, container) { 52 | let ret = `Error validating ${container}.` 53 | let details = err.error.details 54 | 55 | for (let i = 0; i < details.length; i++) { 56 | ret += ` ${details[i].message}.` 57 | } 58 | 59 | return ret 60 | } 61 | 62 | module.exports.createValidator = function generateJoiMiddlewareInstance(cfg) { 63 | cfg = cfg || {} // default to an empty config 64 | // We'll return this instance of the middleware 65 | const instance = { 66 | response 67 | } 68 | 69 | Object.keys(containers).forEach(type => { 70 | // e.g the "body" or "query" from above 71 | const container = containers[type] 72 | 73 | instance[type] = function(schema, opts) { 74 | opts = opts || {} // like config, default to empty object 75 | const computedOpts = { ...container.joi, ...cfg.joi, ...opts.joi } 76 | return function expressJoiValidator(req, res, next) { 77 | const ret = schema.validate(req[type], computedOpts) 78 | 79 | if (!ret.error) { 80 | req[container.storageProperty] = req[type] 81 | const descriptor = Object.getOwnPropertyDescriptor(req, type) 82 | if (descriptor && descriptor.writable) { 83 | req[type] = ret.value 84 | } else { 85 | Object.defineProperty(req, type, { 86 | get() { 87 | return ret.value 88 | } 89 | }) 90 | } 91 | next() 92 | } else if (opts.passError || cfg.passError) { 93 | ret.type = type 94 | next(ret) 95 | } else { 96 | res 97 | .status(opts.statusCode || cfg.statusCode || 400) 98 | .end(buildErrorString(ret, `request ${type}`)) 99 | } 100 | } 101 | } 102 | }) 103 | 104 | return instance 105 | 106 | function response(schema, opts = {}) { 107 | const type = 'response' 108 | return (req, res, next) => { 109 | const resJson = res.json.bind(res) 110 | res.json = validateJson 111 | next() 112 | 113 | function validateJson(json) { 114 | const ret = schema.validate(json, opts.joi) 115 | const { error, value } = ret 116 | if (!error) { 117 | // return res.json ret to retain express compatibility 118 | return resJson(value) 119 | } else if (opts.passError || cfg.passError) { 120 | ret.type = type 121 | next(ret) 122 | } else { 123 | res 124 | .status(opts.statusCode || cfg.statusCode || 500) 125 | .end(buildErrorString(ret, `${type} json`)) 126 | } 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /express-joi-validation.d.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi' 2 | import * as express from 'express' 3 | import { IncomingHttpHeaders } from 'http' 4 | import { ParsedQs } from 'qs' 5 | 6 | /** 7 | * Creates an instance of this module that can be used to generate middleware 8 | * @param cfg 9 | */ 10 | export function createValidator(cfg?: ExpressJoiConfig): ExpressJoiInstance 11 | 12 | /** 13 | * These are the named properties on an express.Request that this module can 14 | * validate, e.g "body" or "query" 15 | */ 16 | export enum ContainerTypes { 17 | Body = 'body', 18 | Query = 'query', 19 | Headers = 'headers', 20 | Fields = 'fields', 21 | Params = 'params' 22 | } 23 | 24 | /** 25 | * Use this in you express error handler if you've set *passError* to true 26 | * when calling *createValidator* 27 | */ 28 | export type ExpressJoiError = Extract< 29 | Joi.ValidationResult, 30 | { error: Joi.ValidationError } 31 | > & { 32 | type: ContainerTypes; 33 | }; 34 | 35 | /** 36 | * A schema that developers should extend to strongly type the properties 37 | * (query, body, etc.) of incoming express.Request passed to a request handler. 38 | */ 39 | export type ValidatedRequestSchema = Record 40 | 41 | /** 42 | * Use this in conjunction with *ValidatedRequestSchema* instead of 43 | * express.Request for route handlers. This ensures *req.query*, 44 | * *req.body* and others are strongly typed using your 45 | * *ValidatedRequestSchema* 46 | */ 47 | export interface ValidatedRequest 48 | extends express.Request { 49 | body: T[ContainerTypes.Body] 50 | query: T[ContainerTypes.Query] & ParsedQs 51 | headers: T[ContainerTypes.Headers] 52 | params: T[ContainerTypes.Params] 53 | } 54 | 55 | /** 56 | * Use this in conjunction with *ValidatedRequestSchema* instead of 57 | * express.Request for route handlers. This ensures *req.query*, 58 | * *req.body* and others are strongly typed using your *ValidatedRequestSchema* 59 | * 60 | * This will also allow you to access the original body, params, etc. as they 61 | * were before validation. 62 | */ 63 | export interface ValidatedRequestWithRawInputsAndFields< 64 | T extends ValidatedRequestSchema 65 | > extends express.Request { 66 | body: T[ContainerTypes.Body] 67 | query: T[ContainerTypes.Query] 68 | headers: T[ContainerTypes.Headers] 69 | params: T[ContainerTypes.Params] 70 | fields: T[ContainerTypes.Fields] 71 | originalBody: any 72 | originalQuery: any 73 | originalHeaders: IncomingHttpHeaders 74 | originalParams: any 75 | originalFields: any 76 | } 77 | 78 | /** 79 | * Configuration options supported by *createValidator(config)* 80 | */ 81 | export interface ExpressJoiConfig { 82 | statusCode?: number 83 | passError?: boolean 84 | joi?: object 85 | } 86 | 87 | /** 88 | * Configuration options supported by middleware, e.g *validator.body(config)* 89 | */ 90 | export interface ExpressJoiContainerConfig { 91 | joi?: Joi.ValidationOptions 92 | statusCode?: number 93 | passError?: boolean 94 | } 95 | 96 | /** 97 | * A validator instance that can be used to generate middleware. Is returned by 98 | * calling *createValidator* 99 | */ 100 | export interface ExpressJoiInstance { 101 | body( 102 | schema: Joi.Schema, 103 | cfg?: ExpressJoiContainerConfig 104 | ): express.RequestHandler 105 | query( 106 | schema: Joi.Schema, 107 | cfg?: ExpressJoiContainerConfig 108 | ): express.RequestHandler 109 | params( 110 | schema: Joi.Schema, 111 | cfg?: ExpressJoiContainerConfig 112 | ): express.RequestHandler 113 | headers( 114 | schema: Joi.Schema, 115 | cfg?: ExpressJoiContainerConfig 116 | ): express.RequestHandler 117 | fields( 118 | schema: Joi.Schema, 119 | cfg?: ExpressJoiContainerConfig 120 | ): express.RequestHandler 121 | response( 122 | schema: Joi.Schema, 123 | cfg?: ExpressJoiContainerConfig 124 | ): express.RequestHandler 125 | } 126 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | const supertest = require('supertest') 5 | const expect = require('chai').expect 6 | 7 | describe('express joi', function() { 8 | var schema, mod 9 | 10 | function getRequester(middleware) { 11 | const app = require('express')() 12 | 13 | // Must apply params middleware inline with the route to match param names 14 | app.get('/params-check/:key', middleware, (req, res) => { 15 | expect(req.params).to.exist 16 | expect(req.originalParams).to.exist 17 | 18 | expect(req.params.key).to.be.a('number') 19 | expect(req.originalParams.key).to.be.a('string') 20 | 21 | res.end('ok') 22 | }) 23 | 24 | // Configure some dummy routes 25 | app.get('/headers-check', middleware, (req, res) => { 26 | expect(req.headers).to.exist 27 | expect(req.originalHeaders).to.exist 28 | 29 | expect(req.headers.key).to.be.a('number') 30 | expect(req.originalHeaders.key).to.be.a('string') 31 | 32 | res.end('ok') 33 | }) 34 | 35 | app.get('/query-check', middleware, (req, res) => { 36 | expect(req.query).to.exist 37 | expect(req.originalQuery).to.exist 38 | 39 | expect(req.query.key).to.be.a('number') 40 | expect(req.originalQuery.key).to.be.a('string') 41 | 42 | res.end('ok') 43 | }) 44 | 45 | app.post( 46 | '/body-check', 47 | require('body-parser').json(), 48 | middleware, 49 | (req, res) => { 50 | expect(req.body).to.exist 51 | expect(req.originalBody).to.exist 52 | 53 | expect(req.body.key).to.be.a('number') 54 | expect(req.originalBody.key).to.be.a('string') 55 | 56 | res.end('ok') 57 | } 58 | ) 59 | 60 | app.post( 61 | '/global-joi-config', 62 | require('body-parser').json(), 63 | middleware, 64 | (req, res) => { 65 | expect(req.body).to.exist 66 | expect(req.originalBody).to.exist 67 | 68 | expect(req.originalBody.known).to.exist 69 | expect(req.originalBody.known).to.exist 70 | 71 | expect(req.originalBody.unknown).to.exist 72 | expect(req.originalBody.unknown).to.exist 73 | 74 | res.end('ok') 75 | } 76 | ) 77 | 78 | app.post( 79 | '/fields-check', 80 | require('express-formidable')(), 81 | middleware, 82 | (req, res) => { 83 | expect(req.fields).to.exist 84 | expect(req.originalFields).to.exist 85 | 86 | expect(req.fields.key).to.be.a('number') 87 | expect(req.originalFields.key).to.be.a('string') 88 | 89 | res.end('ok') 90 | } 91 | ) 92 | app.get('/response/:key', middleware, (req, res) => { 93 | const { key } = req.params 94 | res.json({ key: +key || 'none' }) 95 | }) 96 | 97 | return supertest(app) 98 | } 99 | 100 | beforeEach(function() { 101 | require('clear-require').all() 102 | 103 | schema = Joi.object({ 104 | key: Joi.number() 105 | .integer() 106 | .min(1) 107 | .max(10) 108 | .required() 109 | }) 110 | 111 | mod = require('./express-joi-validation.js').createValidator() 112 | }) 113 | 114 | describe('#headers', function() { 115 | it('should return a 200 since our request is valid', function(done) { 116 | const mw = mod.headers(schema) 117 | 118 | getRequester(mw) 119 | .get('/headers-check') 120 | .expect(200) 121 | .set('key', '10') 122 | .end(done) 123 | }) 124 | 125 | it('should return a 400 since our request is invalid', function(done) { 126 | const mw = mod.headers(schema) 127 | 128 | getRequester(mw) 129 | .get('/headers-check') 130 | .expect(400) 131 | .set('key', '150') 132 | .end(function(err, res) { 133 | expect(res.text).to.contain('"key" must be less than or equal to 10') 134 | done() 135 | }) 136 | }) 137 | }) 138 | 139 | describe('#query', function() { 140 | it('should return a 200 since our querystring is valid', function(done) { 141 | const mw = mod.query(schema) 142 | 143 | getRequester(mw) 144 | .get('/query-check?key=5') 145 | .expect(200) 146 | .end(done) 147 | }) 148 | 149 | it('should return a 400 since our querystring is invalid', function(done) { 150 | const mw = mod.query(schema) 151 | 152 | getRequester(mw) 153 | .get('/query-check') 154 | .expect(400) 155 | .end(function(err, res) { 156 | expect(res.text).to.contain('"key" is required') 157 | done() 158 | }) 159 | }) 160 | }) 161 | 162 | describe('#body', function() { 163 | it('should return a 200 since our body is valid', function(done) { 164 | const mw = mod.body(schema) 165 | 166 | getRequester(mw) 167 | .post('/body-check') 168 | .send({ 169 | key: '1' 170 | }) 171 | .expect(200) 172 | .end(done) 173 | }) 174 | 175 | it('should return a 400 since our body is invalid', function(done) { 176 | const mw = mod.body(schema) 177 | 178 | getRequester(mw) 179 | .post('/body-check') 180 | .expect(400) 181 | .end(function(err, res) { 182 | expect(res.text).to.contain('"key" is required') 183 | done() 184 | }) 185 | }) 186 | }) 187 | 188 | describe('#fields', function() { 189 | it('should return a 200 since our fields are valid', function(done) { 190 | const mw = mod.fields(schema) 191 | 192 | getRequester(mw) 193 | .post('/fields-check') 194 | .field('key', '1') 195 | .expect(200) 196 | .end(done) 197 | }) 198 | 199 | it('should return a 400 since our body is invalid', function(done) { 200 | const mw = mod.fields(schema) 201 | 202 | getRequester(mw) 203 | .post('/fields-check') 204 | .expect(400) 205 | .end(function(err, res) { 206 | expect(res.text).to.contain('"key" is required') 207 | done() 208 | }) 209 | }) 210 | }) 211 | 212 | describe('#params', function() { 213 | it('should return a 200 since our request param is valid', function(done) { 214 | const mw = mod.params(schema) 215 | 216 | getRequester(mw) 217 | .get('/params-check/3') 218 | .expect(200) 219 | .end(done) 220 | }) 221 | 222 | it('should return a 400 since our param is invalid', function(done) { 223 | const mw = mod.params(schema) 224 | 225 | getRequester(mw) 226 | .get('/params-check/not-a-number') 227 | .expect(400) 228 | .end(function(err, res) { 229 | expect(res.text).to.contain('"key" must be a number') 230 | done() 231 | }) 232 | }) 233 | }) 234 | 235 | describe('#response', function() { 236 | it('should return a 500 when the key is not valid', function() { 237 | const middleware = mod.response(schema) 238 | return getRequester(middleware) 239 | .get('/response/one') 240 | .expect(500) 241 | }) 242 | 243 | it('should return a 200 when the key is valid', function() { 244 | const middleware = mod.response(schema) 245 | return getRequester(middleware) 246 | .get('/response/1') 247 | .expect(200) 248 | }) 249 | 250 | it('should pass an error to subsequent handler if it is asked', function() { 251 | const middleware = mod.response(schema, { 252 | passError: true 253 | }) 254 | return getRequester(middleware) 255 | .get('/response/one') 256 | .expect(500) 257 | }) 258 | 259 | it('should return an alternative status for failure', function() { 260 | const middleware = mod.response(schema, { 261 | statusCode: 422 262 | }) 263 | return getRequester(middleware) 264 | .get('/response/one') 265 | .expect(422) 266 | }) 267 | }) 268 | 269 | describe('optional configs', function() { 270 | it('should call next on error via config.passError', function(done) { 271 | const mod = require('./express-joi-validation.js').createValidator({ 272 | passError: true 273 | }) 274 | const mw = mod.query( 275 | Joi.object({ 276 | key: Joi.string() 277 | .required() 278 | .valid('only-this-is-valid') 279 | }) 280 | ) 281 | 282 | mw({ query: { key: 'not valid' } }, {}, err => { 283 | expect(err.type).to.equal('query') 284 | expect(err.error.isJoi).to.be.true 285 | expect(err.value).to.be.an('object') 286 | done() 287 | }) 288 | }) 289 | }) 290 | 291 | describe('#joiGlobalOptionMerging.', function() { 292 | it('should return a 200 since our body is valid', function(done) { 293 | const mod = require('./express-joi-validation.js').createValidator({ 294 | passError: true, 295 | joi: { 296 | allowUnknown: true 297 | } 298 | }) 299 | const schema = Joi.object({ 300 | known: Joi.boolean().required() 301 | }) 302 | 303 | const mw = mod.body(schema) 304 | 305 | getRequester(mw) 306 | .post('/global-joi-config') 307 | .send({ 308 | known: true, 309 | unknown: true 310 | }) 311 | .expect(200) 312 | .end(done) 313 | }) 314 | }) 315 | }) 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-joi-validation 2 | 3 | ![TravisCI](https://travis-ci.org/evanshortiss/express-joi-validation.svg) 4 | [![Coverage Status](https://coveralls.io/repos/github/evanshortiss/express-joi-validation/badge.svg?branch=master)](https://coveralls.io/github/evanshortiss/express-joi-validation?branch=master) 5 | [![npm version](https://badge.fury.io/js/express-joi-validation.svg)](https://www.npmjs.com/package/express-joi-validation) 6 | [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-blue.svg)](http://www.typescriptlang.org/) 7 | [![npm downloads](https://img.shields.io/npm/dm/express-joi-validation.svg?style=flat)](https://www.npmjs.com/package/express-joi-validation) 8 | [![Known Vulnerabilities](https://snyk.io//test/github/evanshortiss/express-joi-validation/badge.svg?targetFile=package.json)](https://snyk.io//test/github/evanshortiss/express-joi-validation?targetFile=package.json) 9 | 10 | A middleware for validating express inputs using Joi schemas. Features include: 11 | 12 | * TypeScript support. 13 | * Specify the order in which request inputs are validated. 14 | * Replaces the incoming `req.body`, `req.query`, etc and with the validated result 15 | * Retains the original `req.body` inside a new property named `req.originalBody`. 16 | . The same applies for headers, query, and params using the `original` prefix, 17 | e.g `req.originalQuery` 18 | * Chooses sensible default Joi options for headers, params, query, and body. 19 | * Uses `peerDependencies` to get a Joi instance of your choosing instead of 20 | using a fixed version. 21 | 22 | ## Quick Links 23 | 24 | * [API](#api) 25 | * [Usage (JavaScript)](#usage-javascript) 26 | * [Usage (TypeScript)](#usage-typescript) 27 | * [Behaviours](#behaviours) 28 | * [Joi Versioning](#joi-versioning) 29 | * [Validation Ordering](#validation-ordering) 30 | * [Error Handling](#error-handling) 31 | * [Joi Options](#joi-options) 32 | * [Custom Express Error Handler](#custom-express-error-handler) 33 | 34 | ## Install 35 | 36 | You need to install `joi` with this module since it relies on it in 37 | `peerDependencies`. 38 | 39 | ``` 40 | npm i express-joi-validation joi --save 41 | ``` 42 | 43 | ## Example 44 | A JavaScript and TypeScript example can be found in the `example/` folder of 45 | this repository. 46 | 47 | ## Usage (JavaScript) 48 | 49 | ```js 50 | const Joi = require('joi') 51 | const app = require('express')() 52 | const validator = require('express-joi-validation').createValidator({}) 53 | 54 | const querySchema = Joi.object({ 55 | name: Joi.string().required() 56 | }) 57 | 58 | app.get('/orders', validator.query(querySchema), (req, res) => { 59 | // If we're in here then the query was valid! 60 | res.end(`Hello ${req.query.name}!`) 61 | }) 62 | ``` 63 | 64 | ## Usage (TypeScript) 65 | 66 | For TypeScript a helper `ValidatedRequest` and 67 | `ValidatedRequestWithRawInputsAndFields` type is provided. This extends the 68 | `express.Request` type and allows you to pass a schema using generics to 69 | ensure type safety in your handler function. 70 | 71 | ```ts 72 | import * as Joi from 'joi' 73 | import * as express from 'express' 74 | import { 75 | ContainerTypes, 76 | // Use this as a replacement for express.Request 77 | ValidatedRequest, 78 | // Extend from this to define a valid schema type/interface 79 | ValidatedRequestSchema, 80 | // Creates a validator that generates middlewares 81 | createValidator 82 | } from 'express-joi-validation' 83 | 84 | const app = express() 85 | const validator = createValidator() 86 | 87 | const querySchema = Joi.object({ 88 | name: Joi.string().required() 89 | }) 90 | 91 | interface HelloRequestSchema extends ValidatedRequestSchema { 92 | [ContainerTypes.Query]: { 93 | name: string 94 | } 95 | } 96 | 97 | app.get( 98 | '/hello', 99 | validator.query(querySchema), 100 | (req: ValidatedRequest, res) => { 101 | // Woohoo, type safety and intellisense for req.query! 102 | res.end(`Hello ${req.query.name}!`) 103 | } 104 | ) 105 | ``` 106 | 107 | You can minimise some duplication by using [joi-extract-type](https://github.com/TCMiranda/joi-extract-type/). 108 | 109 | _NOTE: this does not work with Joi v16+ at the moment. See [this issue](https://github.com/TCMiranda/joi-extract-type/issues/23)._ 110 | 111 | ```ts 112 | import * as Joi from 'joi' 113 | import * as express from 'express' 114 | import { 115 | // Use this as a replacement for express.Request 116 | ValidatedRequest, 117 | // Extend from this to define a valid schema type/interface 118 | ValidatedRequestSchema, 119 | // Creates a validator that generates middlewares 120 | createValidator 121 | } from 'express-joi-validation' 122 | 123 | // This is optional, but without it you need to manually generate 124 | // a type or interface for ValidatedRequestSchema members 125 | import 'joi-extract-type' 126 | 127 | const app = express() 128 | const validator = createValidator() 129 | 130 | const querySchema = Joi.object({ 131 | name: Joi.string().required() 132 | }) 133 | 134 | interface HelloRequestSchema extends ValidatedRequestSchema { 135 | [ContainerTypes.Query]: Joi.extractType 136 | 137 | // Without Joi.extractType you would do this: 138 | // query: { 139 | // name: string 140 | // } 141 | } 142 | 143 | app.get( 144 | '/hello', 145 | validator.query(querySchema), 146 | (req: ValidatedRequest, res) => { 147 | // Woohoo, type safety and intellisense for req.query! 148 | res.end(`Hello ${req.query.name}!`) 149 | } 150 | ) 151 | ``` 152 | 153 | ## API 154 | 155 | ### Structure 156 | 157 | * module (express-joi-validation) 158 | * [createValidator(config)](#createvalidatorconfig) 159 | * [query(options)](#validatorqueryschema-options) 160 | * [body(options)](#validatorbodyschema-options) 161 | * [headers(options)](#validatorheadersschema-options) 162 | * [params(options)](#validatorparamsschema-options) 163 | * [response(options)](#validatorresponseschema-options) 164 | * [fields(options)](#validatorfieldsschema-options) 165 | 166 | ### createValidator(config) 167 | Creates a validator. Supports the following options: 168 | 169 | * passError (default: `false`) - Passes validation errors to the express error 170 | hander using `next(err)` when `true` 171 | * statusCode (default: `400`) - The status code used when validation fails and 172 | `passError` is `false`. 173 | 174 | #### validator.query(schema, [options]) 175 | Creates a middleware instance that will validate the `req.query` for an 176 | incoming request. Can be passed `options` that override the config passed 177 | when the validator was created. 178 | 179 | Supported options are: 180 | 181 | * joi - Custom options to pass to `Joi.validate`. 182 | * passError - Same as above. 183 | * statusCode - Same as above. 184 | 185 | #### validator.body(schema, [options]) 186 | Creates a middleware instance that will validate the `req.body` for an incoming 187 | request. Can be passed `options` that override the options passed when the 188 | validator was created. 189 | 190 | Supported options are the same as `validator.query`. 191 | 192 | #### validator.headers(schema, [options]) 193 | Creates a middleware instance that will validate the `req.headers` for an 194 | incoming request. Can be passed `options` that override the options passed 195 | when the validator was created. 196 | 197 | Supported options are the same as `validator.query`. 198 | 199 | #### validator.params(schema, [options]) 200 | Creates a middleware instance that will validate the `req.params` for an 201 | incoming request. Can be passed `options` that override the options passed 202 | when the validator was created. 203 | 204 | Supported options are the same as `validator.query`. 205 | 206 | #### validator.response(schema, [options]) 207 | Creates a middleware instance that will validate the outgoing response. 208 | Can be passed `options` that override the options passed when the instance was 209 | created. 210 | 211 | Supported options are the same as `validator.query`. 212 | 213 | #### validator.fields(schema, [options]) 214 | Creates a middleware instance that will validate the fields for an incoming 215 | request. This is designed for use with `express-formidable`. Can be passed 216 | `options` that override the options passed when the validator was created. 217 | 218 | The `instance.params` middleware is a little different to the others. It _must_ 219 | be attached directly to the route it is related to. Here's a sample: 220 | 221 | ```js 222 | const schema = Joi.object({ 223 | id: Joi.number().integer().required() 224 | }); 225 | 226 | // INCORRECT 227 | app.use(validator.params(schema)); 228 | app.get('/orders/:id', (req, res, next) => { 229 | // The "id" parameter will NOT have been validated here! 230 | }); 231 | 232 | // CORRECT 233 | app.get('/orders/:id', validator.params(schema), (req, res, next) => { 234 | // This WILL have a validated "id" 235 | }) 236 | ``` 237 | 238 | Supported options are the same as `validator.query`. 239 | 240 | ## Behaviours 241 | 242 | ### Joi Versioning 243 | This module uses `peerDependencies` for the Joi version being used. 244 | This means whatever `joi` version is in the `dependencies` of your 245 | `package.json` will be used by this module. 246 | 247 | 248 | ### Validation Ordering 249 | Validation can be performed in a specific order using standard express 250 | middleware behaviour. Pass the middleware in the desired order. 251 | 252 | Here's an example where the order is headers, body, query: 253 | 254 | ```js 255 | route.get( 256 | '/tickets', 257 | validator.headers(headerSchema), 258 | validator.body(bodySchema), 259 | validator.query(querySchema), 260 | routeHandler 261 | ); 262 | ``` 263 | 264 | ### Error Handling 265 | When validation fails, this module will default to returning a HTTP 400 with 266 | the Joi validation error as a `text/plain` response type. 267 | 268 | A `passError` option is supported to override this behaviour. This option 269 | forces the middleware to pass the error to the express error handler using the 270 | standard `next` function behaviour. 271 | 272 | See the [Custom Express Error Handler](#custom-express-error-handler) section 273 | for an example. 274 | 275 | ### Joi Options 276 | It is possible to pass specific Joi options to each validator like so: 277 | 278 | ```js 279 | route.get( 280 | '/tickets', 281 | validator.headers( 282 | headerSchema, 283 | { 284 | joi: {convert: true, allowUnknown: true} 285 | } 286 | ), 287 | validator.body( 288 | bodySchema, 289 | { 290 | joi: {convert: true, allowUnknown: false} 291 | } 292 | ) 293 | routeHandler 294 | ); 295 | ``` 296 | 297 | The following sensible defaults for Joi are applied if none are passed: 298 | 299 | #### Query 300 | * convert: true 301 | * allowUnknown: false 302 | * abortEarly: false 303 | 304 | #### Body 305 | * convert: true 306 | * allowUnknown: false 307 | * abortEarly: false 308 | 309 | #### Headers 310 | * convert: true 311 | * allowUnknown: true 312 | * stripUnknown: false 313 | * abortEarly: false 314 | 315 | #### Route Params 316 | * convert: true 317 | * allowUnknown: false 318 | * abortEarly: false 319 | 320 | #### Fields (with express-formidable) 321 | * convert: true 322 | * allowUnknown: false 323 | * abortEarly: false 324 | 325 | 326 | ## Custom Express Error Handler 327 | 328 | ```js 329 | const validator = require('express-joi-validation').createValidator({ 330 | // This options forces validation to pass any errors the express 331 | // error handler instead of generating a 400 error 332 | passError: true 333 | }); 334 | 335 | const app = require('express')(); 336 | const orders = require('lib/orders'); 337 | 338 | app.get('/orders', validator.query(require('./query-schema')), (req, res, next) => { 339 | // if we're in here then the query was valid! 340 | orders.getForQuery(req.query) 341 | .then((listOfOrders) => res.json(listOfOrders)) 342 | .catch(next); 343 | }); 344 | 345 | // After your routes add a standard express error handler. This will be passed the Joi 346 | // error, plus an extra "type" field so we can tell what type of validation failed 347 | app.use((err, req, res, next) => { 348 | if (err && err.error && err.error.isJoi) { 349 | // we had a joi error, let's return a custom 400 json response 350 | res.status(400).json({ 351 | type: err.type, // will be "query" here, but could be "headers", "body", or "params" 352 | message: err.error.toString() 353 | }); 354 | } else { 355 | // pass on to another error handler 356 | next(err); 357 | } 358 | }); 359 | ``` 360 | 361 | In TypeScript environments `err.type` can be verified against the exported 362 | `ContainerTypes`: 363 | 364 | ```ts 365 | import { ContainerTypes } from 'express-joi-validation' 366 | 367 | app.use((err: any|ExpressJoiError, req: express.Request, res: express.Response, next: express.NextFunction) => { 368 | // ContainerTypes is an enum exported by this module. It contains strings 369 | // such as "body", "headers", "query"... 370 | if (err && 'type' in err && err.type in ContainerTypes) { 371 | const e: ExpressJoiError = err 372 | // e.g "You submitted a bad query paramater" 373 | res.status(400).end(`You submitted a bad ${e.type} paramater`) 374 | } else { 375 | res.status(500).end('internal server error') 376 | } 377 | }) 378 | ``` 379 | 380 | --------------------------------------------------------------------------------