├── .gitignore ├── README.md ├── app.js ├── middlewares └── SchemaValidator.js ├── package-lock.json ├── package.json ├── routes.js └── schemas.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # joi-schema-validation-sourcecode 2 | 3 | **You can checkout the complete tutorial on Scotch: [Node API Schema Validation with Joi](https://scotch.io/tutorials/node-api-schema-validation-with-joi).** 4 | 5 | Source code for tutorial on Node API Schema Validation with Joi. In order to run the demo on your local machine and experiment with the source code, do the following: 6 | 7 | - Clone the repository into a new directory on your machine 8 | 9 | - Run the following command from the new directory to install the dependencies: 10 | 11 | ```sh 12 | npm install 13 | ``` 14 | 15 | - The app will run on port **3000** by default. If you would prefer another port, you can specify it in the `app.js` file. Look for the following line in the file and replace `3000` with your desired port. 16 | 17 | ```js 18 | const port = process.env.NODE_ENV || 3000; 19 | ``` 20 | 21 | - Finally, start the demo app by running the following command: 22 | 23 | ```sh 24 | npm start 25 | ``` 26 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // load app dependencies 2 | const express = require('express'); 3 | const logger = require('morgan'); 4 | const bodyParser = require('body-parser'); 5 | 6 | const Routes = require('./routes'); 7 | 8 | const app = express(); 9 | const port = process.env.NODE_ENV || 3000; 10 | 11 | // app configurations 12 | app.set('port', port); 13 | 14 | // load app middlewares 15 | app.use(logger('dev')); 16 | app.use(bodyParser.json()); 17 | app.use(bodyParser.urlencoded({ extended: false })); 18 | 19 | // load our API routes 20 | app.use('/', Routes); 21 | 22 | app.post('/test', (req, res, next) => { 23 | 24 | // require the Joi module 25 | const Joi = require('joi'); 26 | 27 | // fetch the request data 28 | const data = req.body; 29 | 30 | // define the validation schema 31 | const schema = Joi.object().keys({ 32 | 33 | // email is required 34 | // email must be a valid email string 35 | email: Joi.string().email().required(), 36 | 37 | // phone is required 38 | // and must be a string of the format XXX-XXX-XXXX 39 | // where X is a digit (0-9) 40 | phone: Joi.string().regex(/^\d{3}-\d{3}-\d{4}$/).required(), 41 | 42 | // birthday is not required 43 | // birthday must be a valid ISO-8601 date 44 | // dates before Jan 1, 2014 are not allowed 45 | birthday: Joi.date().max('1-1-2004').iso(), 46 | 47 | }); 48 | 49 | // validate the request data against the schema 50 | Joi.validate(data, schema, (err, value) => { 51 | 52 | // create a random number as id 53 | const id = Math.ceil(Math.random() * 9999999); 54 | 55 | if (err) { 56 | // send a 422 error response if validation fails 57 | res.status(422).json({ 58 | status: 'error', 59 | message: 'Invalid request data', 60 | data: data 61 | }); 62 | } else { 63 | // send a success response if validation passes 64 | // attach the random ID to the data response 65 | res.json({ 66 | status: 'success', 67 | message: 'User created successfully', 68 | data: Object.assign({id}, value) 69 | }); 70 | } 71 | 72 | }); 73 | 74 | }); 75 | 76 | // establish http server connection 77 | app.listen(port, () => { console.log(`App running on port ${port}`) }); 78 | -------------------------------------------------------------------------------- /middlewares/SchemaValidator.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Joi = require('joi'); 3 | const Schemas = require('../schemas'); 4 | 5 | module.exports = (useJoiError = false) => { 6 | // useJoiError determines if we should respond with the base Joi error 7 | // boolean: defaults to false 8 | const _useJoiError = _.isBoolean(useJoiError) && useJoiError; 9 | 10 | // enabled HTTP methods for request data validation 11 | const _supportedMethods = ['post', 'put']; 12 | 13 | // Joi validation options 14 | const _validationOptions = { 15 | abortEarly: false, // abort after the last validation error 16 | allowUnknown: true, // allow unknown keys that will be ignored 17 | stripUnknown: true // remove unknown keys from the validated data 18 | }; 19 | 20 | // return the validation middleware 21 | return (req, res, next) => { 22 | 23 | const route = req.route.path; 24 | const method = req.method.toLowerCase(); 25 | 26 | if (_.includes(_supportedMethods, method) && _.has(Schemas, route)) { 27 | 28 | // get schema for the current route 29 | const _schema = _.get(Schemas, route); 30 | 31 | if (_schema) { 32 | 33 | // Validate req.body using the schema and validation options 34 | return Joi.validate(req.body, _schema, _validationOptions, (err, data) => { 35 | 36 | if (err) { 37 | 38 | // Joi Error 39 | const JoiError = { 40 | status: 'failed', 41 | error: { 42 | original: err._object, 43 | 44 | // fetch only message and type from each error 45 | details: _.map(err.details, ({message, type}) => ({ 46 | message: message.replace(/['"]/g, ''), 47 | type 48 | })) 49 | } 50 | }; 51 | 52 | // Custom Error 53 | const CustomError = { 54 | status: 'failed', 55 | error: 'Invalid request data. Please review request and try again.' 56 | }; 57 | 58 | // Send back the JSON error response 59 | res.status(422).json(_useJoiError ? JoiError : CustomError); 60 | 61 | } else { 62 | // Replace req.body with the data after Joi validation 63 | req.body = data; 64 | next(); 65 | } 66 | 67 | }); 68 | 69 | } 70 | } 71 | 72 | next(); 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "accepts": { 7 | "version": "1.3.4", 8 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 9 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=" 10 | }, 11 | "array-flatten": { 12 | "version": "1.1.1", 13 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 14 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 15 | }, 16 | "basic-auth": { 17 | "version": "2.0.0", 18 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", 19 | "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=" 20 | }, 21 | "body-parser": { 22 | "version": "1.18.2", 23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 24 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=" 25 | }, 26 | "bytes": { 27 | "version": "3.0.0", 28 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 29 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 30 | }, 31 | "content-disposition": { 32 | "version": "0.5.2", 33 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 34 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 35 | }, 36 | "content-type": { 37 | "version": "1.0.4", 38 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 39 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 40 | }, 41 | "cookie": { 42 | "version": "0.3.1", 43 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 44 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 45 | }, 46 | "cookie-signature": { 47 | "version": "1.0.6", 48 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 49 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 50 | }, 51 | "debug": { 52 | "version": "2.6.9", 53 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 54 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==" 55 | }, 56 | "depd": { 57 | "version": "1.1.1", 58 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 59 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 60 | }, 61 | "destroy": { 62 | "version": "1.0.4", 63 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 64 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 65 | }, 66 | "ee-first": { 67 | "version": "1.1.1", 68 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 69 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 70 | }, 71 | "encodeurl": { 72 | "version": "1.0.1", 73 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", 74 | "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" 75 | }, 76 | "escape-html": { 77 | "version": "1.0.3", 78 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 79 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 80 | }, 81 | "etag": { 82 | "version": "1.8.1", 83 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 84 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 85 | }, 86 | "express": { 87 | "version": "4.16.2", 88 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", 89 | "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", 90 | "dependencies": { 91 | "setprototypeof": { 92 | "version": "1.1.0", 93 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 94 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 95 | }, 96 | "statuses": { 97 | "version": "1.3.1", 98 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 99 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 100 | } 101 | } 102 | }, 103 | "finalhandler": { 104 | "version": "1.1.0", 105 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", 106 | "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", 107 | "dependencies": { 108 | "statuses": { 109 | "version": "1.3.1", 110 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 111 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 112 | } 113 | } 114 | }, 115 | "forwarded": { 116 | "version": "0.1.2", 117 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 118 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 119 | }, 120 | "fresh": { 121 | "version": "0.5.2", 122 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 123 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 124 | }, 125 | "hoek": { 126 | "version": "5.0.2", 127 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.2.tgz", 128 | "integrity": "sha512-NA10UYP9ufCtY2qYGkZktcQXwVyYK4zK0gkaFSB96xhtlo6V8tKXdQgx8eHolQTRemaW0uLn8BhjhwqrOU+QLQ==" 129 | }, 130 | "http-errors": { 131 | "version": "1.6.2", 132 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 133 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=" 134 | }, 135 | "iconv-lite": { 136 | "version": "0.4.19", 137 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 138 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 139 | }, 140 | "inherits": { 141 | "version": "2.0.3", 142 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 143 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 144 | }, 145 | "ipaddr.js": { 146 | "version": "1.5.2", 147 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", 148 | "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" 149 | }, 150 | "isemail": { 151 | "version": "3.0.0", 152 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.0.0.tgz", 153 | "integrity": "sha512-rz0ng/c+fX+zACpLgDB8fnUQ845WSU06f4hlhk4K8TJxmR6f5hyvitu9a9JdMD7aq/P4E0XdG1uaab2OiXgHlA==" 154 | }, 155 | "joi": { 156 | "version": "13.0.2", 157 | "resolved": "https://registry.npmjs.org/joi/-/joi-13.0.2.tgz", 158 | "integrity": "sha512-kVka3LaHQyENvcMW4WJPSepGM43oCofcKxfs9HbbKd/FrwBAAt4lNNTPKOzSMmV53GIspmNO4U3O2TzoGvxxCA==" 159 | }, 160 | "lodash": { 161 | "version": "4.17.4", 162 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 163 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 164 | }, 165 | "media-typer": { 166 | "version": "0.3.0", 167 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 168 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 169 | }, 170 | "merge-descriptors": { 171 | "version": "1.0.1", 172 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 173 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 174 | }, 175 | "methods": { 176 | "version": "1.1.2", 177 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 178 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 179 | }, 180 | "mime": { 181 | "version": "1.4.1", 182 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 183 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 184 | }, 185 | "mime-db": { 186 | "version": "1.30.0", 187 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 188 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 189 | }, 190 | "mime-types": { 191 | "version": "2.1.17", 192 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 193 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=" 194 | }, 195 | "morgan": { 196 | "version": "1.9.0", 197 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", 198 | "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=" 199 | }, 200 | "ms": { 201 | "version": "2.0.0", 202 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 203 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 204 | }, 205 | "negotiator": { 206 | "version": "0.6.1", 207 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 208 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 209 | }, 210 | "on-finished": { 211 | "version": "2.3.0", 212 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 213 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" 214 | }, 215 | "on-headers": { 216 | "version": "1.0.1", 217 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", 218 | "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" 219 | }, 220 | "parseurl": { 221 | "version": "1.3.2", 222 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 223 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 224 | }, 225 | "path-to-regexp": { 226 | "version": "0.1.7", 227 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 228 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 229 | }, 230 | "proxy-addr": { 231 | "version": "2.0.2", 232 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", 233 | "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=" 234 | }, 235 | "punycode": { 236 | "version": "2.1.0", 237 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", 238 | "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" 239 | }, 240 | "qs": { 241 | "version": "6.5.1", 242 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 243 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 244 | }, 245 | "range-parser": { 246 | "version": "1.2.0", 247 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 248 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 249 | }, 250 | "raw-body": { 251 | "version": "2.3.2", 252 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 253 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=" 254 | }, 255 | "safe-buffer": { 256 | "version": "5.1.1", 257 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 258 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 259 | }, 260 | "send": { 261 | "version": "0.16.1", 262 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", 263 | "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", 264 | "dependencies": { 265 | "statuses": { 266 | "version": "1.3.1", 267 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 268 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 269 | } 270 | } 271 | }, 272 | "serve-static": { 273 | "version": "1.13.1", 274 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 275 | "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==" 276 | }, 277 | "setprototypeof": { 278 | "version": "1.0.3", 279 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 280 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 281 | }, 282 | "statuses": { 283 | "version": "1.4.0", 284 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 285 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 286 | }, 287 | "topo": { 288 | "version": "3.0.0", 289 | "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", 290 | "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==" 291 | }, 292 | "type-is": { 293 | "version": "1.6.15", 294 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 295 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" 296 | }, 297 | "unpipe": { 298 | "version": "1.0.0", 299 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 300 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 301 | }, 302 | "utils-merge": { 303 | "version": "1.0.1", 304 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 305 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 306 | }, 307 | "vary": { 308 | "version": "1.1.2", 309 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 310 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.18.2", 15 | "express": "^4.16.2", 16 | "joi": "^13.0.2", 17 | "lodash": "^4.17.4", 18 | "morgan": "^1.9.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const SchemaValidator = require('./middlewares/SchemaValidator'); 4 | 5 | // We are using the formatted Joi Validation error 6 | // Pass false as argument to use a generic error 7 | const validateRequest = SchemaValidator(true); 8 | 9 | // generic route handler 10 | const genericHandler = (req, res, next) => { 11 | res.json({ 12 | status: 'success', 13 | data: req.body 14 | }); 15 | }; 16 | 17 | // create a new teacher or student 18 | router.post('/people', validateRequest, genericHandler); 19 | 20 | // change auth credentials for teachers 21 | router.post('/auth/edit', validateRequest, genericHandler); 22 | 23 | // accept fee payments for students 24 | router.post('/fees/pay', validateRequest, genericHandler); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /schemas.js: -------------------------------------------------------------------------------- 1 | // load Joi module 2 | const Joi = require('joi'); 3 | 4 | // accepts name only as letters and converts to uppercase 5 | const name = Joi.string().regex(/^[A-Z]+$/).uppercase(); 6 | 7 | // accepts a valid UUID v4 string as id 8 | const personID = Joi.string().guid({version: 'uuidv4'}); 9 | 10 | // accepts ages greater than 6 11 | // value could be in one of these forms: 15, '15', '15y', '15yr', '15yrs' 12 | // all string ages will be replaced to strip off non-digits 13 | const ageSchema = Joi.alternatives().try([ 14 | Joi.number().integer().greater(6).required(), 15 | Joi.string().replace(/^([7-9]|[1-9]\d+)(y|yr|yrs)?$/i, '$1').required() 16 | ]); 17 | 18 | const personDataSchema = Joi.object().keys({ 19 | id: personID.required(), 20 | firstname: name, 21 | lastname: name, 22 | fullname: Joi.string().regex(/^[A-Z]+ [A-Z]+$/i).uppercase(), 23 | type: Joi.string().valid('STUDENT', 'TEACHER').uppercase().required(), 24 | sex: Joi.string().valid(['M', 'F', 'MALE', 'FEMALE']).uppercase().required(), 25 | 26 | // if type is STUDENT, then age is required 27 | age: Joi.when('type', { 28 | is: 'STUDENT', 29 | then: ageSchema.required(), 30 | otherwise: ageSchema 31 | }) 32 | }) 33 | 34 | // must have only one between firstname and lastname 35 | .xor('firstname', 'fullname') 36 | 37 | // firstname and lastname must always appear together 38 | .and('firstname', 'lastname') 39 | 40 | // firstname and lastname cannot appear together with fullname 41 | .without('fullname', ['firstname', 'lastname']); 42 | 43 | // password and confirmPassword must contain the same value 44 | const authDataSchema = Joi.object({ 45 | teacherId: personID.required(), 46 | email: Joi.string().email().lowercase().required(), 47 | password: Joi.string().min(7).required(), 48 | confirmPassword: Joi.string().valid(Joi.ref('password')).required().strict() 49 | }); 50 | 51 | // cardNumber must be a valid Luhn number 52 | const feesDataSchema = Joi.object({ 53 | studentId: personID.required(), 54 | amount: Joi.number().positive().greater(1).precision(2).required(), 55 | cardNumber: Joi.string().creditCard().required(), 56 | completedAt: Joi.date().timestamp().required() 57 | }); 58 | 59 | // export the schemas 60 | module.exports = { 61 | '/people': personDataSchema, 62 | '/auth/edit': authDataSchema, 63 | '/fees/pay': feesDataSchema 64 | }; 65 | --------------------------------------------------------------------------------