express-zod-safe 1.3.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -5
- package/dist/{index.js → cjs/index.js} +15 -4
- package/dist/{index.d.ts → esm/index.d.ts} +13 -15
- package/dist/esm/index.js +95 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ _This package was inspired by Aquila169's [zod-express-middleware](https://githu
|
|
|
15
15
|
## 🔒 Features
|
|
16
16
|
|
|
17
17
|
- **Typesafe**: Built with TypeScript, offering complete typesafe interfaces that enrich your development experience.
|
|
18
|
-
- **Zod Integration**: Utilizes Zod schemas for comprehensive and
|
|
18
|
+
- **Zod Integration**: Utilizes Zod schemas for comprehensive and customisable request validation.
|
|
19
19
|
- **Middleware Flexibility**: Easily integrates with Express.js middleware stack, ensuring a smooth validation process without compromising performance.
|
|
20
20
|
- **Parameter & Query Validation**: Validates not just request bodies but also URL parameters and query strings, covering all facets of incoming data.
|
|
21
21
|
- **Error Handling**: Provides detailed, developer-friendly error responses to aid in debugging and informing API consumers.
|
|
@@ -87,6 +87,40 @@ app.post('/user/:userId', validate({ handler, params, query, body }), (req, res)
|
|
|
87
87
|
});
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
### ⚠️ Usage with Additional Middleware
|
|
91
|
+
When using `express-zod-safe` with other middleware, it is important not to explicitly type the `Request` parameter in the middleware, as this will override the inferred type that `express-zod-safe` generates from your validation schemas. The best way to do this is to instead type your other middleware (or cast them) to `WeakRequestHandler`, a weakly typed version of the `RequestHandler` type from `express`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import validate, { type WeakRequestHandler } from 'express-zod-safe';
|
|
95
|
+
|
|
96
|
+
// Use the RequestHandler type, instead of explicitly typing (req: Request, res: Response, next: NextFunction)
|
|
97
|
+
const authenticate: WeakRequestHandler = (req, res, next) => {
|
|
98
|
+
// ... perform user authentication
|
|
99
|
+
|
|
100
|
+
next();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
app.post('/user/:userId', authenticate, validate({ params, query, body }), (req, res) => {
|
|
104
|
+
// Your validation typing will work as expected here
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If you do not control the middleware, such as when you import it from another library, you can instead cast the middleware to `WeakRequestHandler`.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// For one off cases...
|
|
113
|
+
app.post('/user/:userId', authenticate as WeakRequestHandler, validate({ params, query, body }), (req, res) => {
|
|
114
|
+
// Your validation typing will work as expected here
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// For a middleware with a lot of use, aliasing the middleware...
|
|
118
|
+
const auth = authenticate as WeakRequestHandler;
|
|
119
|
+
app.post('/user/:userId', auth, validate({ params, query, body }), (req, res) => {
|
|
120
|
+
// Your validation typing will work as expected here
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
90
124
|
### ⚠️ URL Parameters & Query Strings Coercion
|
|
91
125
|
As mentioned in the example above, all URL parameters and query strings are parsed as strings. This means that if you have a URL parameter or query string that is expected to be a number, you must use the `z.coerce.number()` method to coerce the value to a number. This is because Zod will not coerce the value for you, and will instead throw an error if the value is not a string.
|
|
92
126
|
|
|
@@ -101,7 +135,7 @@ app.get('/user/:userId', validate({ params }), (req, res) => {
|
|
|
101
135
|
```
|
|
102
136
|
|
|
103
137
|
### ⚠️ Missing Validation Schemas
|
|
104
|
-
If you do not provide a validation schema for a particular request component (e.g. `params`, `query`, or `body`), then that component will be assumed to be empty. This means that requests with non-empty components will be rejected, and requests with empty components will be accepted. The types on the `req` object will also reflect this, and will be `
|
|
138
|
+
If you do not provide a validation schema for a particular request component (e.g. `params`, `query`, or `body`), then that component will be assumed to be empty. This means that requests with non-empty components will be rejected, and requests with empty components will be accepted. The types on the `req` object will also reflect this, and will be an empty object `{}` if the component is not provided.
|
|
105
139
|
|
|
106
140
|
```ts
|
|
107
141
|
const body = {
|
|
@@ -112,12 +146,28 @@ const body = {
|
|
|
112
146
|
app.post('/user', validate({ body }), (req, res) => {
|
|
113
147
|
// req.body.name -> string
|
|
114
148
|
// req.body.email -> string
|
|
115
|
-
// req.params.age ->
|
|
116
|
-
// req.query.age ->
|
|
149
|
+
// req.params.age -> Property 'age' does not exist on type '{}'
|
|
150
|
+
// req.query.age -> Property 'age' does not exist on type '{}'
|
|
117
151
|
});
|
|
118
152
|
```
|
|
119
153
|
|
|
120
|
-
This behaviour is intentional and ensures that you do not try to access or use a property that does not exist on the `req` object.
|
|
154
|
+
This behaviour is intentional and ensures that you do not try to access or use a property that does not exist on the `req` object. If you'd prefer to allow any property for any given request component, you can do so by setting a loose validation schema with `z.any()`.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const body = {
|
|
158
|
+
name: z.string(),
|
|
159
|
+
email: z.string().email(),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const params = z.any()
|
|
163
|
+
|
|
164
|
+
app.post('/user', validate({ body, params }), (req, res) => {
|
|
165
|
+
// req.body.name -> string
|
|
166
|
+
// req.body.email -> string
|
|
167
|
+
// req.params.age -> any
|
|
168
|
+
// req.query.age -> Property 'age' does not exist on type '{}'
|
|
169
|
+
});
|
|
170
|
+
```
|
|
121
171
|
|
|
122
172
|
## ⭐️ Show your support
|
|
123
173
|
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
13
|
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.default = validate;
|
|
5
16
|
const express_1 = __importDefault(require("express"));
|
|
6
17
|
const zod_1 = require("zod");
|
|
7
18
|
const types = ['query', 'params', 'body'];
|
|
@@ -12,7 +23,7 @@ const emptyObjectSchema = zod_1.z.object({}).strict();
|
|
|
12
23
|
* @returns Whether the provided schema is a ZodSchema.
|
|
13
24
|
*/
|
|
14
25
|
function isZodSchema(schema) {
|
|
15
|
-
return !!schema && typeof schema.
|
|
26
|
+
return !!schema && typeof schema.safeParseAsync === 'function';
|
|
16
27
|
}
|
|
17
28
|
// Override express@^5 request.query getter to provider setter
|
|
18
29
|
const descriptor = Object.getOwnPropertyDescriptor(express_1.default.request, 'query');
|
|
@@ -77,12 +88,12 @@ function validate(schemas) {
|
|
|
77
88
|
query: isZodSchema(schemas.query) ? schemas.query : zod_1.z.object((_b = schemas.query) !== null && _b !== void 0 ? _b : {}).strict(),
|
|
78
89
|
body: isZodSchema(schemas.body) ? schemas.body : zod_1.z.object((_c = schemas.body) !== null && _c !== void 0 ? _c : {}).strict()
|
|
79
90
|
};
|
|
80
|
-
return (req, res, next) => {
|
|
91
|
+
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
81
92
|
var _a;
|
|
82
93
|
const errors = [];
|
|
83
94
|
// Validate all types (params, query, body)
|
|
84
95
|
for (const type of types) {
|
|
85
|
-
const parsed = validation[type].
|
|
96
|
+
const parsed = yield validation[type].safeParseAsync((_a = req[type]) !== null && _a !== void 0 ? _a : {});
|
|
86
97
|
if (parsed.success)
|
|
87
98
|
req[type] = parsed.data;
|
|
88
99
|
else
|
|
@@ -97,6 +108,6 @@ function validate(schemas) {
|
|
|
97
108
|
return;
|
|
98
109
|
}
|
|
99
110
|
return next();
|
|
100
|
-
};
|
|
111
|
+
});
|
|
101
112
|
}
|
|
102
113
|
module.exports = validate;
|
|
@@ -2,7 +2,7 @@ import type { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
|
2
2
|
import { type ZodError, type ZodRawShape, type ZodTypeAny, z } from 'zod';
|
|
3
3
|
declare const types: readonly ["query", "params", "body"];
|
|
4
4
|
declare const emptyObjectSchema: z.ZodObject<{}, "strict", ZodTypeAny, {}, {}>;
|
|
5
|
-
type
|
|
5
|
+
export type EmptyValidationSchema = typeof emptyObjectSchema;
|
|
6
6
|
/**
|
|
7
7
|
* Generates a middleware function for Express.js that validates request params, query, and body.
|
|
8
8
|
* This function uses Zod schemas to perform validation against the provided schema definitions.
|
|
@@ -41,7 +41,7 @@ type Empty = typeof emptyObjectSchema;
|
|
|
41
41
|
*
|
|
42
42
|
* app.listen(3000, () => console.log('Server running on port 3000'));
|
|
43
43
|
*/
|
|
44
|
-
|
|
44
|
+
export default function validate<TParams extends ValidationSchema = EmptyValidationSchema, TQuery extends ValidationSchema = EmptyValidationSchema, TBody extends ValidationSchema = EmptyValidationSchema>(schemas: CompleteValidationSchema<TParams, TQuery, TBody>): RequestHandler<ZodOutput<TParams>, any, ZodOutput<TBody>, ZodOutput<TQuery>>;
|
|
45
45
|
/**
|
|
46
46
|
* Describes the types of data that can be validated: 'query', 'params', or 'body'.
|
|
47
47
|
*/
|
|
@@ -50,25 +50,19 @@ type DataType = (typeof types)[number];
|
|
|
50
50
|
* Defines the structure of an error item, containing the type of validation that failed (params, query, or body)
|
|
51
51
|
* and the associated ZodError.
|
|
52
52
|
*/
|
|
53
|
-
interface ErrorListItem {
|
|
53
|
+
export interface ErrorListItem {
|
|
54
54
|
type: DataType;
|
|
55
55
|
errors: ZodError;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Represents
|
|
58
|
+
* Represents an Express.js error request handler where the params, query and body are of unknown type as validation failed.
|
|
59
59
|
*/
|
|
60
|
-
|
|
61
|
-
[key: string]: undefined | string | string[] | ParsedQs | ParsedQs[];
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Represents an Express.js error request handler.
|
|
65
|
-
*/
|
|
66
|
-
type ErrorRequestHandler<P = Record<string, string>, ResBody = any, ReqBody = any, ReqQuery = ParsedQs, LocalsObj extends Record<string, any> = Record<string, any>> = (err: ErrorListItem[], req: Request<P, ResBody, ReqBody, ReqQuery, LocalsObj>, res: Response<ResBody, LocalsObj>, next: NextFunction) => void | Promise<void>;
|
|
60
|
+
export type ErrorRequestHandler<P = unknown, ResBody = any, ReqBody = unknown, ReqQuery = unknown, LocalsObj extends Record<string, any> = Record<string, any>> = (err: ErrorListItem[], req: Request<P, ResBody, ReqBody, ReqQuery, LocalsObj>, res: Response<ResBody, LocalsObj>, next: NextFunction) => void | Promise<void>;
|
|
67
61
|
/**
|
|
68
62
|
* Represents a generic type for route validation, which can be applied to params, query, or body.
|
|
69
63
|
* Each key-value pair represents a field and its corresponding Zod validation schema.
|
|
70
64
|
*/
|
|
71
|
-
type
|
|
65
|
+
export type ValidationSchema = ZodTypeAny | ZodRawShape;
|
|
72
66
|
/**
|
|
73
67
|
* Defines the structure for the schemas provided to the validate middleware.
|
|
74
68
|
* Each property corresponds to a different part of the request (params, query, body)
|
|
@@ -78,7 +72,7 @@ type Validation = ZodTypeAny | ZodRawShape;
|
|
|
78
72
|
* @template TQuery - Type definition for query schema.
|
|
79
73
|
* @template TBody - Type definition for body schema.
|
|
80
74
|
*/
|
|
81
|
-
interface
|
|
75
|
+
export interface CompleteValidationSchema<TParams extends ValidationSchema = EmptyValidationSchema, TQuery extends ValidationSchema = EmptyValidationSchema, TBody extends ValidationSchema = EmptyValidationSchema> {
|
|
82
76
|
handler?: ErrorRequestHandler;
|
|
83
77
|
params?: TParams;
|
|
84
78
|
query?: TQuery;
|
|
@@ -91,5 +85,9 @@ interface ExtendedValidationSchemas<TParams, TQuery, TBody> {
|
|
|
91
85
|
*
|
|
92
86
|
* @template T - The validation type (params, query, or body).
|
|
93
87
|
*/
|
|
94
|
-
type ZodOutput<T extends
|
|
95
|
-
|
|
88
|
+
export type ZodOutput<T extends ValidationSchema> = T extends ZodRawShape ? z.ZodObject<T>['_output'] : T['_output'];
|
|
89
|
+
/**
|
|
90
|
+
* A utility type to ensure other middleware types don't conflict with the validate middleware.
|
|
91
|
+
*/
|
|
92
|
+
export type WeakRequestHandler = RequestHandler<unknown, unknown, unknown, Record<string, unknown>>;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const types = ['query', 'params', 'body'];
|
|
4
|
+
const emptyObjectSchema = z.object({}).strict();
|
|
5
|
+
/**
|
|
6
|
+
* A ZodSchema type guard.
|
|
7
|
+
* @param schema The Zod schema to check.
|
|
8
|
+
* @returns Whether the provided schema is a ZodSchema.
|
|
9
|
+
*/
|
|
10
|
+
function isZodSchema(schema) {
|
|
11
|
+
return !!schema && typeof schema.safeParseAsync === 'function';
|
|
12
|
+
}
|
|
13
|
+
// Override express@^5 request.query getter to provider setter
|
|
14
|
+
const descriptor = Object.getOwnPropertyDescriptor(express.request, 'query');
|
|
15
|
+
if (descriptor) {
|
|
16
|
+
Object.defineProperty(express.request, 'query', {
|
|
17
|
+
get() {
|
|
18
|
+
if (this._query)
|
|
19
|
+
return this._query;
|
|
20
|
+
return descriptor?.get?.call(this);
|
|
21
|
+
},
|
|
22
|
+
set(query) {
|
|
23
|
+
this._query = query;
|
|
24
|
+
},
|
|
25
|
+
configurable: true,
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generates a middleware function for Express.js that validates request params, query, and body.
|
|
31
|
+
* This function uses Zod schemas to perform validation against the provided schema definitions.
|
|
32
|
+
*
|
|
33
|
+
* @param schemas - An object containing Zod schemas for params, query, and body. Optional handler for custom error handling.
|
|
34
|
+
* @returns An Express.js middleware function that validates the request based on the provided schemas.
|
|
35
|
+
* It attaches validated data to the request object and sends error details if validation fails.
|
|
36
|
+
* @template TParams - Type definition for params schema.
|
|
37
|
+
* @template TQuery - Type definition for query schema.
|
|
38
|
+
* @template TBody - Type definition for body schema.
|
|
39
|
+
* @example
|
|
40
|
+
* // Example usage in an Express.js route
|
|
41
|
+
* import express from 'express';
|
|
42
|
+
* import validate from 'express-zod-safe';
|
|
43
|
+
* import { z } from 'zod';
|
|
44
|
+
*
|
|
45
|
+
* const app = express();
|
|
46
|
+
*
|
|
47
|
+
* // Define your Zod schemas
|
|
48
|
+
* const params = {
|
|
49
|
+
* userId: z.string().uuid(),
|
|
50
|
+
* };
|
|
51
|
+
* const query = {
|
|
52
|
+
* age: z.coerce.number().optional(),
|
|
53
|
+
* };
|
|
54
|
+
* const body = {
|
|
55
|
+
* name: z.string(),
|
|
56
|
+
* email: z.string().email(),
|
|
57
|
+
* };
|
|
58
|
+
*
|
|
59
|
+
* // Use the validate middleware in your route
|
|
60
|
+
* app.post('/user/:userId', validate({ params, query, body }), (req, res) => {
|
|
61
|
+
* // Your route logic here
|
|
62
|
+
* res.send('User data is valid!');
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* app.listen(3000, () => console.log('Server running on port 3000'));
|
|
66
|
+
*/
|
|
67
|
+
export default function validate(schemas) {
|
|
68
|
+
// Create validation objects for each type
|
|
69
|
+
const validation = {
|
|
70
|
+
params: isZodSchema(schemas.params) ? schemas.params : z.object(schemas.params ?? {}).strict(),
|
|
71
|
+
query: isZodSchema(schemas.query) ? schemas.query : z.object(schemas.query ?? {}).strict(),
|
|
72
|
+
body: isZodSchema(schemas.body) ? schemas.body : z.object(schemas.body ?? {}).strict()
|
|
73
|
+
};
|
|
74
|
+
return async (req, res, next) => {
|
|
75
|
+
const errors = [];
|
|
76
|
+
// Validate all types (params, query, body)
|
|
77
|
+
for (const type of types) {
|
|
78
|
+
const parsed = await validation[type].safeParseAsync(req[type] ?? {});
|
|
79
|
+
if (parsed.success)
|
|
80
|
+
req[type] = parsed.data;
|
|
81
|
+
else
|
|
82
|
+
errors.push({ type, errors: parsed.error });
|
|
83
|
+
}
|
|
84
|
+
// Return all errors if there are any
|
|
85
|
+
if (errors.length > 0) {
|
|
86
|
+
// If a custom error handler is provided, use it
|
|
87
|
+
if (schemas.handler)
|
|
88
|
+
return schemas.handler(errors, req, res, next);
|
|
89
|
+
res.status(400).send(errors.map(error => ({ type: error.type, errors: error.errors })));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
return next();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
module.exports = validate;
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-zod-safe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "TypeScript-friendly middleware designed for Express applications, leveraging the robustness of Zod schemas to validate incoming request bodies, parameters, and queries.",
|
|
5
|
-
"main": "dist/index.js",
|
|
5
|
+
"main": "dist/cjs/index.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
6
8
|
"repository": {
|
|
7
9
|
"type": "git",
|
|
8
10
|
"url": "git+https://github.com/AngaBlue/express-zod-safe.git"
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
},
|
|
37
39
|
"scripts": {
|
|
38
40
|
"clean": "rimraf dist",
|
|
39
|
-
"build": "pnpm run clean && tsc",
|
|
40
|
-
"lint": "biome check --fix
|
|
41
|
+
"build": "pnpm run clean && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
|
|
42
|
+
"lint": "biome check --fix"
|
|
41
43
|
}
|
|
42
44
|
}
|