|
| 1 | +# @sliit-foss/zelebrate |
| 2 | + |
| 3 | +### Express middleware which wraps around the [Zod](https://www.npmjs.com/package/zod) validation library to provide a simple way to validate request bodies, query parameters, and headers. Heavy inspiration from [celebrate](https://www.npmjs.com/package/celebrate) which is a middleware for the `Joi` validation library. |
| 4 | + |
| 5 | +#### Zelebrate exposes the same API as [celebrate](https://www.npmjs.com/package/celebrate), but uses Zod for validation instead of Joi. This means that you can use Zelebrate in the same way you would use celebrate, but with the added benefits of Zod's type inference and validation capabilities. |
| 6 | + |
| 7 | +#### Further this library exposes a utility function named `zelebrateStack` which preserves the request segment types within the express handlers at lower levels automatically. Use this to build rock solid typesafe express applications. |
| 8 | + |
| 9 | +## Installation |
| 10 | + |
| 11 | +```js |
| 12 | +# using npm |
| 13 | +npm install @sliit-foss/zelebrate |
| 14 | + |
| 15 | +# using yarn |
| 16 | +yarn add @sliit-foss/zelebrate |
| 17 | +``` |
| 18 | + |
| 19 | +## Usage |
| 20 | + |
| 21 | +```js |
| 22 | +const express = require("express"); |
| 23 | +const bodyParser = require("body-parser"); |
| 24 | +const { zelebrate, z, errors, Segments } = require("@sliit-foss/zelebrate"); |
| 25 | + |
| 26 | +const app = express(); |
| 27 | + |
| 28 | +app.use(bodyParser.json()); |
| 29 | + |
| 30 | +app.post( |
| 31 | + "/signup", |
| 32 | + zelebrate({ |
| 33 | + [Segments.BODY]: z.object({ |
| 34 | + name: z.string(), |
| 35 | + age: z.number().int(), |
| 36 | + role: z.string().default("admin") |
| 37 | + }), |
| 38 | + [Segments.QUERY]: z.object({ |
| 39 | + token: z.string().uuid() |
| 40 | + }) |
| 41 | + }), |
| 42 | + (req, res) => { |
| 43 | + // At this point, req.body has been validated and |
| 44 | + // req.body.role is equal to req.body.role if provided in the POST or set to 'admin' by joi |
| 45 | + } |
| 46 | +); |
| 47 | +app.use(errors()); |
| 48 | +``` |
| 49 | + |
| 50 | +## Type Safe Express Handlers |
| 51 | + |
| 52 | +```js |
| 53 | +const express = require("express"); |
| 54 | +const bodyParser = require("body-parser"); |
| 55 | +const { zelebrateStack, z, errors, Segments } = require("@sliit-foss/zelebrate"); |
| 56 | + |
| 57 | +const app = express(); |
| 58 | + |
| 59 | +app.use(bodyParser.json()); |
| 60 | + |
| 61 | +app.post( |
| 62 | + "/signup", |
| 63 | + zelebrateStack({ |
| 64 | + [Segments.BODY]: z.object({ |
| 65 | + name: z.string(), |
| 66 | + age: z.number().int(), |
| 67 | + role: z.string().default("admin") |
| 68 | + }), |
| 69 | + [Segments.QUERY]: z.object({ |
| 70 | + token: z.string().uuid() |
| 71 | + }) |
| 72 | + })( |
| 73 | + /** Add any number of handlers. req.body and req.query will be typed within all of them */ |
| 74 | + (req, res) => { |
| 75 | + // req.body and req.query are type safe. |
| 76 | + } |
| 77 | + ) |
| 78 | +); |
| 79 | +app.use(errors()); |
| 80 | +``` |
| 81 | + |
| 82 | +## API |
| 83 | + |
| 84 | +zelebrate does not have a default export. The following methods encompass the public API. |
| 85 | + |
| 86 | +### `zelebrate(schema, [opts])` |
| 87 | + |
| 88 | +Returns a `function` with the middleware signature (`(req, res, next)`). |
| 89 | + |
| 90 | +- `requestRules` - an `object` where `key` can be one of the values from [`Segments`](#segments) and the `value` is a zod validation schema. Only the keys specified will be validated against the incoming request object. If you omit a key, that part of the `req` object will not be validated. A schema must contain at least one valid key. |
| 91 | +- `[opts]` - an optional `object` with the following keys. Defaults to `{}`. |
| 92 | + - `mode` - optional [`Modes`](#modes) for controlling the validation mode zelebrate uses. Defaults to `partial`. |
| 93 | + |
| 94 | +### `zelebrator([opts], [joiOptions], schema)` |
| 95 | + |
| 96 | +This is a curried version of `zelebrate` It is curried with `lodash.curryRight` so it can be called in all the various fashions that [API supports](https://lodash.com/docs/4.17.15#curryRight). Returns a `function` with the middleware signature (`(req, res, next)`). |
| 97 | + |
| 98 | +- `[opts]` - an optional `object` with the following keys. Defaults to `{}`. |
| 99 | + - `mode` - optional [`Modes`](#modes) for controlling the validation mode zelebrate uses. Defaults to `partial`. |
| 100 | +- `requestRules` - an `object` where `key` can be one of the values from [`Segments`](#segments) and the `value` is a zod validation schema. Only the keys specified will be validated against the incoming request object. If you omit a key, that part of the `req` object will not be validated. A schema must contain at least one valid key. |
| 101 | + |
| 102 | +<details> |
| 103 | + <summary>Sample usage</summary> |
| 104 | + |
| 105 | +This is an example use of curried zelebrate in a real server. |
| 106 | + |
| 107 | +```js |
| 108 | +const express = require("express"); |
| 109 | +const { zelebrator, z, errors, Segments } = require("@sliit-foss/zelebrate"); |
| 110 | +const app = express(); |
| 111 | + |
| 112 | +// now every instance of `zelebrate` will use these same options so you only |
| 113 | +// need to do it once. |
| 114 | +const zelebrate = zelebrator({ mode: Modes.FULL }); |
| 115 | + |
| 116 | +// validate all incoming request headers for the token header |
| 117 | +// if missing or not the correct format, respond with an error |
| 118 | +app.use( |
| 119 | + zelebrate({ |
| 120 | + [Segments.HEADERS]: z |
| 121 | + .object({ |
| 122 | + token: z.string().regex(/abc\d{3}/) |
| 123 | + }) |
| 124 | + .catchall(z.unknown()) |
| 125 | + }) |
| 126 | +); |
| 127 | + |
| 128 | +app.get( |
| 129 | + "/", |
| 130 | + zelebrate({ |
| 131 | + [Segments.HEADERS]: z |
| 132 | + .object({ |
| 133 | + name: Joi.string() |
| 134 | + }) |
| 135 | + .catchall(z.unknown()) |
| 136 | + }), |
| 137 | + (req, res) => { |
| 138 | + res.send("hello world"); |
| 139 | + } |
| 140 | +); |
| 141 | + |
| 142 | +app.use(errors()); |
| 143 | +``` |
| 144 | + |
| 145 | +</details> |
| 146 | + |
| 147 | +### `errors([opts])` |
| 148 | + |
| 149 | +Returns a `function` with the error handler signature (`(err, req, res, next)`). This should be placed with any other error handling middleware to catch zelebrate errors. If the incoming `err` object is an error originating from zelebrate, `errors()` will respond a pre-build error object. Otherwise, it will call `next(err)` and will pass the error along and will need to be processed by another error handler. |
| 150 | + |
| 151 | +- `[opts]` - an optional `object` with the following keys |
| 152 | + - `statusCode` - `number` that will be used for the response status code in the event of an error. Must be greater than 399 and less than 600. It must also be a number available to the node [HTTP module](https://nodejs.org/api/http.html#http_http_status_codes). Defaults to 400. |
| 153 | + - `message` - `string` that will be used for the `message` value sent out by the error handler. Defaults to `'Validation failed'` |
| 154 | + |
| 155 | +If the error response format does not suite your needs, you are encouraged to write your own and check `isZelebrateError(err)` to format zelebrate errors to your liking. |
| 156 | + |
| 157 | +Zelebrate augments the error objects returned by `zod` and adds a `pretty` method to it. This method will return a human readable string of the first error in the validation chain. |
| 158 | + |
| 159 | +Errors origintating from the `zelebrate()` middleware are `ZelebrateError` objects. |
| 160 | + |
| 161 | +### `z` |
| 162 | + |
| 163 | +zelebrate exports the version of zod it is using internally. For maximum compatibility, you should use this version when creating schemas used with zelebrate. |
| 164 | + |
| 165 | +### `Segments` |
| 166 | + |
| 167 | +An enum containing all the segments of `req` objects that zelebrate _can_ validate against. |
| 168 | + |
| 169 | +```js |
| 170 | +{ |
| 171 | + BODY: 'body', |
| 172 | + COOKIES: 'cookies', |
| 173 | + HEADERS: 'headers', |
| 174 | + PARAMS: 'params', |
| 175 | + QUERY: 'query', |
| 176 | + SIGNEDCOOKIES: 'signedCookies', |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +### `Modes` |
| 181 | + |
| 182 | +An enum containing all the available validation modes that zelebrate can support. |
| 183 | + |
| 184 | +- `PARTIAL` - ends validation on the first failure. |
| 185 | +- `FULL` - validates the entire request object and collects all the validation failures in the result. |
| 186 | + |
| 187 | +### `new ZelebrateError([message])` |
| 188 | + |
| 189 | +Creates a new `ZelebrateError` object. Extends the built in `Error` object. |
| 190 | + |
| 191 | +- `message` - optional `string` message. Defaults to `'Validation failed'`. |
| 192 | + |
| 193 | +`ZelebrateError` has the following public properties: |
| 194 | + |
| 195 | +- `details` - a `Map` of all validation failures. The `key` is a [`Segments`](#segments) and the value is a zod validation error. |
| 196 | + |
| 197 | +### `isZelebrateError(err)` |
| 198 | + |
| 199 | +Returns `true` if the provided `err` object originated from the `zelebrate` middleware, and `false` otherwise. Useful if you want to write your own error handler for zelebrate errors. |
| 200 | + |
| 201 | +- `err` - an error object |
| 202 | + |
| 203 | +## Additional Details |
| 204 | + |
| 205 | +### Validation Order |
| 206 | + |
| 207 | +zelebrate validates request values in the following order: |
| 208 | + |
| 209 | +1. `req.headers` |
| 210 | +2. `req.params` |
| 211 | +3. `req.query` |
| 212 | +4. `req.cookies` (_assuming `cookie-parser` is being used_) |
| 213 | +5. `req.signedCookies` (_assuming `cookie-parser` is being used_) |
| 214 | +6. `req.body` (_assuming `body-parser` is being used_) |
| 215 | + |
| 216 | +### Mutation Warning |
| 217 | + |
| 218 | +If you use any of zods's transformation APIs (`transform`, `coerce`, etc.) `zelebrate` will override the source value with the changes applied by the transformation |
| 219 | + |
| 220 | +For example, if you validate `req.query` and have a `default` value in your zod schema, if the incoming `req.query` is missing a value for default, during validation `zelebrate` will overwrite the original `req.query` with the transformed result. |
| 221 | + |
| 222 | +### Additional Info |
| 223 | + |
| 224 | +According the the HTTP spec, `GET` requests should _not_ include a body in the request payload. For that reason, `zelebrate` does not validate the body on `GET` requests. |
| 225 | + |
| 226 | +## Issues |
| 227 | + |
| 228 | +_Before_ opening issues on this repo, make sure your zod schema is correct and working as you intended. The bulk of this code is just exposing the zod API as express middleware. All of the heavy lifting still happens inside zod. |
0 commit comments