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 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 customizable request validation.
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 `undefined` if the component is not provided.
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 -> undefined
116
- // req.query.age -> undefined
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.safeParse === 'function';
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].safeParse((_a = req[type]) !== null && _a !== void 0 ? _a : {});
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 Empty = typeof emptyObjectSchema;
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
- declare function validate<TParams extends Validation = Empty, TQuery extends Validation = Empty, TBody extends Validation = Empty>(schemas: ExtendedValidationSchemas<TParams, TQuery, TBody>): RequestHandler<ZodOutput<TParams>, any, ZodOutput<TBody>, ZodOutput<TQuery>>;
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 a QueryString object parsed by the qs library.
58
+ * Represents an Express.js error request handler where the params, query and body are of unknown type as validation failed.
59
59
  */
60
- interface ParsedQs {
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 Validation = ZodTypeAny | ZodRawShape;
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 ExtendedValidationSchemas<TParams, TQuery, TBody> {
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 Validation> = T extends ZodRawShape ? z.ZodObject<T>['_output'] : T['_output'];
95
- export = validate;
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.3",
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 --error-on-warnings"
41
+ "build": "pnpm run clean && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
42
+ "lint": "biome check --fix"
41
43
  }
42
44
  }