Skip to content

Commit 5a0cd2c

Browse files
Merge pull request #227 from sliit-foss/feature/zelebrate
Release stable version of @sliit-foss/zelebrate
2 parents 24892ff + 92c3504 commit 5a0cd2c

File tree

10 files changed

+682
-1
lines changed

10 files changed

+682
-1
lines changed

packages/zelebrate/package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@sliit-foss/zelebrate",
3+
"version": "0.0.0",
4+
"description": "Express middleware which wraps around the zod validation library to provide a simple way to validate request bodies, query parameters, and headers.",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"build": "node ../../scripts/esbuild.config.js && bash ../../scripts/build-types.sh",
8+
"build:watch": "bash ../../scripts/esbuild.watch.sh",
9+
"bump-version": "bash ../../scripts/bump-version.sh --name=@sliit-foss/zelebrate",
10+
"lint": "bash ../../scripts/lint.sh",
11+
"release": "bash ../../scripts/release.sh",
12+
"test": "bash ../../scripts/test/test.sh"
13+
},
14+
"dependencies": {
15+
"escape-html": "1.0.3",
16+
"lodash": "4.17.21",
17+
"zod": "3.24.3"
18+
},
19+
"optionalDependencies": {
20+
"express": "*"
21+
},
22+
"peerDependencies": {
23+
"express": "*"
24+
},
25+
"devDependencies": {
26+
"@types/lodash": "4.17.15"
27+
},
28+
"author": "SLIIT FOSS",
29+
"license": "MIT",
30+
"repository": {
31+
"type": "git",
32+
"url": "git+https://github.com/sliit-foss/npm-catalogue.git"
33+
},
34+
"homepage": "https://github.com/sliit-foss/npm-catalogue/blob/main/packages/zelebrate/readme.md",
35+
"keywords": [
36+
"zod",
37+
"validation",
38+
"express",
39+
"middleware"
40+
],
41+
"bugs": {
42+
"url": "https://github.com/sliit-foss/npm-catalogue/issues"
43+
}
44+
}

packages/zelebrate/readme.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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.

packages/zelebrate/src/constants.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export enum Segments {
2+
PARAMS = "params",
3+
HEADERS = "headers",
4+
QUERY = "query",
5+
COOKIES = "cookies",
6+
SIGNEDCOOKIES = "signedCookies",
7+
BODY = "body"
8+
}
9+
10+
export enum Modes {
11+
FULL = "full",
12+
PARTIAL = "partial"
13+
}

packages/zelebrate/src/error.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ZodError } from "zod";
2+
import { Segments } from "./constants";
3+
4+
declare module "zod" {
5+
interface ZodError {
6+
/**
7+
* Formats and returns the first issue in a human-readable way.
8+
*/
9+
pretty(): string;
10+
}
11+
}
12+
13+
ZodError.prototype.pretty = function () {
14+
if (this.issues.length === 0) {
15+
return "No issues found";
16+
}
17+
return this.issues[0].expected
18+
? `Expected ${this.issues[0].expected} but received ${this.issues[0].received} for \`${this.issues[0].path.join(".")}\``
19+
: this.issues[0].message;
20+
};
21+
22+
export class ZelebrateError extends Error {
23+
public details: Map<Segments, ZodError>;
24+
constructor(message = "Validation failed") {
25+
super(message);
26+
this.details = new Map();
27+
}
28+
}
29+
30+
export const isZelebrateError = (err: any) => err instanceof ZelebrateError;

packages/zelebrate/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./constants";
2+
export * from "./zelebrate";
3+
export * from "./error";

packages/zelebrate/src/schema.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { z } from "zod";
2+
import { Segments, Modes } from "./constants";
3+
4+
export type RequestRules = {
5+
[K in Segments]?: z.ZodObject<any>;
6+
};
7+
8+
export interface ZelebrateOptions {
9+
mode?: Modes;
10+
}
11+
12+
export interface ErrorOptions {
13+
statusCode?: number;
14+
message?: string;
15+
}

0 commit comments

Comments
 (0)