express-zod-routes 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # express-zod-routes
2
+
3
+ [![CI](https://github.com/adriangrahldev/express-zod-routes/actions/workflows/ci.yml/badge.svg)](https://github.com/adriangrahldev/express-zod-routes/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/express-zod-routes.svg)](https://www.npmjs.com/package/express-zod-routes)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ **Express middleware** that validates `body`, `query`, and `params` with **[Zod](https://zod.dev)**. On success, parsed values land on **`req.validated`**. On failure, the client gets a **consistent JSON 400** (or your chosen status) without calling `next`.
8
+
9
+ Built for **MERN / TypeScript** stacks: one source of truth for validation, no duplicate DTO logic.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install express-zod-routes
15
+ ```
16
+
17
+ Peer dependencies:
18
+
19
+ - `express` `^4.18.0 || ^5.0.0`
20
+ - `zod` `^3.22.0`
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import express from 'express';
26
+ import { z } from 'zod';
27
+ import { validate } from 'express-zod-routes';
28
+
29
+ const app = express();
30
+ app.use(express.json());
31
+
32
+ app.post(
33
+ '/users',
34
+ validate({
35
+ body: z.object({
36
+ name: z.string().min(1),
37
+ email: z.string().email(),
38
+ }),
39
+ }),
40
+ (req, res) => {
41
+ // req.validated.body is parsed & typed with InferValidated (see below)
42
+ res.status(201).json(req.validated!.body);
43
+ }
44
+ );
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `validate(schemas)` · `createValidator(schemas)`
50
+
51
+ Same function; `createValidator` is an alias.
52
+
53
+ | Option | Type | Description |
54
+ | -------------- | --------- | ------------------------------------------------- |
55
+ | `body` | `ZodType` | Validates `req.body` (use after `express.json()`) |
56
+ | `query` | `ZodType` | Validates `req.query` (strings → use `z.coerce`) |
57
+ | `params` | `ZodType` | Validates `req.params` |
58
+ | `statusCode` | `number` | Default `400` |
59
+ | `errorMessage` | `string` | Default `"Validation failed"` |
60
+
61
+ At least one of `body`, `query`, or `params` is required.
62
+
63
+ ### `req.validated`
64
+
65
+ After the middleware runs successfully, `req.validated` contains only the keys you validated (merged if you stack multiple validators on the same route — usually one per route is enough).
66
+
67
+ Importing the package augments Express’s `Request` so `validated` is recognized by TypeScript.
68
+
69
+ ### Typing handlers with `InferValidated`
70
+
71
+ ```ts
72
+ import type { Request } from 'express';
73
+ import { validate, type InferValidated } from 'express-zod-routes';
74
+
75
+ const schemas = {
76
+ body: z.object({ email: z.string().email() }),
77
+ } as const;
78
+
79
+ app.post(
80
+ '/',
81
+ validate(schemas),
82
+ (req: Request & { validated: InferValidated<typeof schemas> }, res) => {
83
+ req.validated.body.email;
84
+ res.sendStatus(204);
85
+ }
86
+ );
87
+ ```
88
+
89
+ ### Error response shape
90
+
91
+ ```json
92
+ {
93
+ "error": "Validation failed",
94
+ "issues": [{ "path": "email", "message": "Invalid email", "code": "invalid_string" }]
95
+ }
96
+ ```
97
+
98
+ ### `mapZodErrorToResponse(err, message?)`
99
+
100
+ Use in your own error middleware if you reuse the same JSON shape.
101
+
102
+ ## Query & params tips
103
+
104
+ - **Query** values are strings (or arrays) from Express. Use `z.coerce.number()`, `z.enum()`, etc.
105
+ - **Params** are strings; use `z.string().uuid()`, regex, or transforms as needed.
106
+
107
+ ## Comparison
108
+
109
+ | Approach | express-zod-routes |
110
+ | --------------------- | ------------------------ |
111
+ | Manual `if` + res 400 | Centralized Zod + format |
112
+ | Only body validation | body + query + params |
113
+
114
+ Other great options exist (`express-zod-api`, custom Zod wrappers). This package stays small: one middleware, `req.validated`, stable errors.
115
+
116
+ ## Example project
117
+
118
+ See [`examples/basic-express`](examples/basic-express).
119
+
120
+ ## Contributing
121
+
122
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
123
+
124
+ ## License
125
+
126
+ MIT © see [LICENSE](LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var DEFAULT_VALIDATION_ERROR_MESSAGE = "Validation failed";
5
+ var DEFAULT_VALIDATION_STATUS = 400;
6
+
7
+ // src/middleware/error-mapper.ts
8
+ function issuePath(issue) {
9
+ return issue.path.map(String).join(".") || "(root)";
10
+ }
11
+ function mapZodErrorToResponse(err, message = DEFAULT_VALIDATION_ERROR_MESSAGE) {
12
+ return {
13
+ error: message,
14
+ issues: err.issues.map((issue) => ({
15
+ path: issuePath(issue),
16
+ message: issue.message,
17
+ code: issue.code
18
+ }))
19
+ };
20
+ }
21
+
22
+ // src/middleware/validate.ts
23
+ function parseSegment(schema, raw, errorMessage, statusCode, res) {
24
+ const result = schema.safeParse(raw);
25
+ if (result.success) {
26
+ return { ok: true, data: result.data };
27
+ }
28
+ res.status(statusCode).json(mapZodErrorToResponse(result.error, errorMessage));
29
+ return { ok: false };
30
+ }
31
+ function validate(schemas) {
32
+ const keys = ["body", "query", "params"];
33
+ const hasAny = keys.some((k) => schemas[k] !== void 0);
34
+ if (!hasAny) {
35
+ throw new Error(
36
+ "express-zod-routes: validate() requires at least one of body, query, or params"
37
+ );
38
+ }
39
+ const statusCode = schemas.statusCode ?? DEFAULT_VALIDATION_STATUS;
40
+ const errorMessage = schemas.errorMessage ?? DEFAULT_VALIDATION_ERROR_MESSAGE;
41
+ return (req, res, next) => {
42
+ try {
43
+ const prev = req.validated ?? {};
44
+ const nextValidated = { ...prev };
45
+ if (schemas.body) {
46
+ const out = parseSegment(schemas.body, req.body, errorMessage, statusCode, res);
47
+ if (!out.ok) return;
48
+ nextValidated.body = out.data;
49
+ }
50
+ if (schemas.query) {
51
+ const out = parseSegment(schemas.query, req.query, errorMessage, statusCode, res);
52
+ if (!out.ok) return;
53
+ nextValidated.query = out.data;
54
+ }
55
+ if (schemas.params) {
56
+ const out = parseSegment(schemas.params, req.params, errorMessage, statusCode, res);
57
+ if (!out.ok) return;
58
+ nextValidated.params = out.data;
59
+ }
60
+ req.validated = nextValidated;
61
+ next();
62
+ } catch (e) {
63
+ next(e);
64
+ }
65
+ };
66
+ }
67
+ var createValidator = validate;
68
+
69
+ exports.DEFAULT_VALIDATION_ERROR_MESSAGE = DEFAULT_VALIDATION_ERROR_MESSAGE;
70
+ exports.DEFAULT_VALIDATION_STATUS = DEFAULT_VALIDATION_STATUS;
71
+ exports.createValidator = createValidator;
72
+ exports.mapZodErrorToResponse = mapZodErrorToResponse;
73
+ exports.validate = validate;
74
+ //# sourceMappingURL=index.cjs.map
75
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/middleware/error-mapper.ts","../src/middleware/validate.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** Default message for validation error responses */\nexport const DEFAULT_VALIDATION_ERROR_MESSAGE = 'Validation failed' as const;\n\n/** Default HTTP status when validation fails */\nexport const DEFAULT_VALIDATION_STATUS = 400 as const;\n","import type { ZodError, ZodIssue } from 'zod';\nimport { DEFAULT_VALIDATION_ERROR_MESSAGE } from '../constants.js';\n\n/** One validation issue in the API response */\nexport interface ValidationIssue {\n /** Dot-separated path (e.g. `user.email`) */\n path: string;\n message: string;\n code: ZodIssue['code'];\n}\n\n/** Stable JSON body for HTTP 400 validation errors */\nexport interface ValidationErrorBody {\n error: string;\n issues: ValidationIssue[];\n}\n\nfunction issuePath(issue: ZodIssue): string {\n return issue.path.map(String).join('.') || '(root)';\n}\n\n/**\n * Maps a Zod error to the package’s standard error payload (for custom error middleware).\n */\nexport function mapZodErrorToResponse(\n err: ZodError,\n message: string = DEFAULT_VALIDATION_ERROR_MESSAGE\n): ValidationErrorBody {\n return {\n error: message,\n issues: err.issues.map((issue) => ({\n path: issuePath(issue),\n message: issue.message,\n code: issue.code,\n })),\n };\n}\n","import type { NextFunction, Request, RequestHandler, Response } from 'express';\nimport type { ZodTypeAny } from 'zod';\nimport { DEFAULT_VALIDATION_ERROR_MESSAGE, DEFAULT_VALIDATION_STATUS } from '../constants.js';\nimport type { ValidationSchemas } from '../types/schemas.js';\nimport { mapZodErrorToResponse } from './error-mapper.js';\n\nexport interface ValidateOptions extends ValidationSchemas {\n /** HTTP status when validation fails (default: 400) */\n statusCode?: number;\n /** Override default error message in JSON body */\n errorMessage?: string;\n}\n\nfunction parseSegment(\n schema: ZodTypeAny,\n raw: unknown,\n errorMessage: string,\n statusCode: number,\n res: Response\n): { ok: true; data: unknown } | { ok: false } {\n const result = schema.safeParse(raw);\n if (result.success) {\n return { ok: true, data: result.data };\n }\n res.status(statusCode).json(mapZodErrorToResponse(result.error, errorMessage));\n return { ok: false };\n}\n\n/**\n * Express middleware: validates `body`, `query`, and/or `params` with Zod.\n * On success, merges results into `req.validated`. On failure, sends JSON 400 and does not call `next`.\n *\n * @example\n * ```ts\n * app.post(\n * '/users',\n * express.json(),\n * validate({ body: z.object({ name: z.string().min(1) }) }),\n * (req, res) => res.json(req.validated!.body)\n * );\n * ```\n */\nexport function validate(schemas: ValidateOptions): RequestHandler {\n const keys = ['body', 'query', 'params'] as const;\n const hasAny = keys.some((k) => schemas[k] !== undefined);\n if (!hasAny) {\n throw new Error(\n 'express-zod-routes: validate() requires at least one of body, query, or params'\n );\n }\n\n const statusCode = schemas.statusCode ?? DEFAULT_VALIDATION_STATUS;\n const errorMessage = schemas.errorMessage ?? DEFAULT_VALIDATION_ERROR_MESSAGE;\n\n return (req: Request, res: Response, next: NextFunction) => {\n try {\n const prev = req.validated ?? {};\n const nextValidated: NonNullable<Request['validated']> = { ...prev };\n\n if (schemas.body) {\n const out = parseSegment(schemas.body, req.body, errorMessage, statusCode, res);\n if (!out.ok) return;\n nextValidated.body = out.data;\n }\n if (schemas.query) {\n const out = parseSegment(schemas.query, req.query, errorMessage, statusCode, res);\n if (!out.ok) return;\n nextValidated.query = out.data;\n }\n if (schemas.params) {\n const out = parseSegment(schemas.params, req.params, errorMessage, statusCode, res);\n if (!out.ok) return;\n nextValidated.params = out.data;\n }\n\n req.validated = nextValidated;\n next();\n } catch (e) {\n next(e);\n }\n };\n}\n\n/** Alias for {@link validate} */\nexport const createValidator = validate;\n"]}
@@ -0,0 +1,99 @@
1
+ import { RequestHandler } from 'express';
2
+ import { z, ZodIssue, ZodError } from 'zod';
3
+
4
+ /**
5
+ * Augments Express `Request` with `validated` when you import `express-zod-routes`.
6
+ */
7
+
8
+ declare global {
9
+ namespace Express {
10
+ interface Request {
11
+ /**
12
+ * Parsed values after {@link validate}. Only keys you passed schemas for are set.
13
+ */
14
+ validated?: {
15
+ body?: unknown;
16
+ query?: unknown;
17
+ params?: unknown;
18
+ };
19
+ }
20
+ }
21
+ }
22
+
23
+ /** Schemas you can pass to {@link validate} */
24
+ interface ValidationSchemas {
25
+ body?: z.ZodTypeAny;
26
+ query?: z.ZodTypeAny;
27
+ params?: z.ZodTypeAny;
28
+ }
29
+ type Empty = Record<never, never>;
30
+ /**
31
+ * Shape of `req.validated` inferred from the schemas passed to {@link validate}.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const schemas = { body: z.object({ email: z.string().email() }) };
36
+ * app.post(
37
+ * '/',
38
+ * validate(schemas),
39
+ * (req: Request & { validated: InferValidated<typeof schemas> }, res) => {
40
+ * req.validated.body.email;
41
+ * }
42
+ * );
43
+ * ```
44
+ */
45
+ type InferValidated<S extends ValidationSchemas> = (S['body'] extends z.ZodTypeAny ? {
46
+ body: z.infer<NonNullable<S['body']>>;
47
+ } : Empty) & (S['query'] extends z.ZodTypeAny ? {
48
+ query: z.infer<NonNullable<S['query']>>;
49
+ } : Empty) & (S['params'] extends z.ZodTypeAny ? {
50
+ params: z.infer<NonNullable<S['params']>>;
51
+ } : Empty);
52
+
53
+ interface ValidateOptions extends ValidationSchemas {
54
+ /** HTTP status when validation fails (default: 400) */
55
+ statusCode?: number;
56
+ /** Override default error message in JSON body */
57
+ errorMessage?: string;
58
+ }
59
+ /**
60
+ * Express middleware: validates `body`, `query`, and/or `params` with Zod.
61
+ * On success, merges results into `req.validated`. On failure, sends JSON 400 and does not call `next`.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * app.post(
66
+ * '/users',
67
+ * express.json(),
68
+ * validate({ body: z.object({ name: z.string().min(1) }) }),
69
+ * (req, res) => res.json(req.validated!.body)
70
+ * );
71
+ * ```
72
+ */
73
+ declare function validate(schemas: ValidateOptions): RequestHandler;
74
+ /** Alias for {@link validate} */
75
+ declare const createValidator: typeof validate;
76
+
77
+ /** One validation issue in the API response */
78
+ interface ValidationIssue {
79
+ /** Dot-separated path (e.g. `user.email`) */
80
+ path: string;
81
+ message: string;
82
+ code: ZodIssue['code'];
83
+ }
84
+ /** Stable JSON body for HTTP 400 validation errors */
85
+ interface ValidationErrorBody {
86
+ error: string;
87
+ issues: ValidationIssue[];
88
+ }
89
+ /**
90
+ * Maps a Zod error to the package’s standard error payload (for custom error middleware).
91
+ */
92
+ declare function mapZodErrorToResponse(err: ZodError, message?: string): ValidationErrorBody;
93
+
94
+ /** Default message for validation error responses */
95
+ declare const DEFAULT_VALIDATION_ERROR_MESSAGE: "Validation failed";
96
+ /** Default HTTP status when validation fails */
97
+ declare const DEFAULT_VALIDATION_STATUS: 400;
98
+
99
+ export { DEFAULT_VALIDATION_ERROR_MESSAGE, DEFAULT_VALIDATION_STATUS, type InferValidated, type ValidateOptions, type ValidationErrorBody, type ValidationIssue, type ValidationSchemas, createValidator, mapZodErrorToResponse, validate };
@@ -0,0 +1,99 @@
1
+ import { RequestHandler } from 'express';
2
+ import { z, ZodIssue, ZodError } from 'zod';
3
+
4
+ /**
5
+ * Augments Express `Request` with `validated` when you import `express-zod-routes`.
6
+ */
7
+
8
+ declare global {
9
+ namespace Express {
10
+ interface Request {
11
+ /**
12
+ * Parsed values after {@link validate}. Only keys you passed schemas for are set.
13
+ */
14
+ validated?: {
15
+ body?: unknown;
16
+ query?: unknown;
17
+ params?: unknown;
18
+ };
19
+ }
20
+ }
21
+ }
22
+
23
+ /** Schemas you can pass to {@link validate} */
24
+ interface ValidationSchemas {
25
+ body?: z.ZodTypeAny;
26
+ query?: z.ZodTypeAny;
27
+ params?: z.ZodTypeAny;
28
+ }
29
+ type Empty = Record<never, never>;
30
+ /**
31
+ * Shape of `req.validated` inferred from the schemas passed to {@link validate}.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const schemas = { body: z.object({ email: z.string().email() }) };
36
+ * app.post(
37
+ * '/',
38
+ * validate(schemas),
39
+ * (req: Request & { validated: InferValidated<typeof schemas> }, res) => {
40
+ * req.validated.body.email;
41
+ * }
42
+ * );
43
+ * ```
44
+ */
45
+ type InferValidated<S extends ValidationSchemas> = (S['body'] extends z.ZodTypeAny ? {
46
+ body: z.infer<NonNullable<S['body']>>;
47
+ } : Empty) & (S['query'] extends z.ZodTypeAny ? {
48
+ query: z.infer<NonNullable<S['query']>>;
49
+ } : Empty) & (S['params'] extends z.ZodTypeAny ? {
50
+ params: z.infer<NonNullable<S['params']>>;
51
+ } : Empty);
52
+
53
+ interface ValidateOptions extends ValidationSchemas {
54
+ /** HTTP status when validation fails (default: 400) */
55
+ statusCode?: number;
56
+ /** Override default error message in JSON body */
57
+ errorMessage?: string;
58
+ }
59
+ /**
60
+ * Express middleware: validates `body`, `query`, and/or `params` with Zod.
61
+ * On success, merges results into `req.validated`. On failure, sends JSON 400 and does not call `next`.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * app.post(
66
+ * '/users',
67
+ * express.json(),
68
+ * validate({ body: z.object({ name: z.string().min(1) }) }),
69
+ * (req, res) => res.json(req.validated!.body)
70
+ * );
71
+ * ```
72
+ */
73
+ declare function validate(schemas: ValidateOptions): RequestHandler;
74
+ /** Alias for {@link validate} */
75
+ declare const createValidator: typeof validate;
76
+
77
+ /** One validation issue in the API response */
78
+ interface ValidationIssue {
79
+ /** Dot-separated path (e.g. `user.email`) */
80
+ path: string;
81
+ message: string;
82
+ code: ZodIssue['code'];
83
+ }
84
+ /** Stable JSON body for HTTP 400 validation errors */
85
+ interface ValidationErrorBody {
86
+ error: string;
87
+ issues: ValidationIssue[];
88
+ }
89
+ /**
90
+ * Maps a Zod error to the package’s standard error payload (for custom error middleware).
91
+ */
92
+ declare function mapZodErrorToResponse(err: ZodError, message?: string): ValidationErrorBody;
93
+
94
+ /** Default message for validation error responses */
95
+ declare const DEFAULT_VALIDATION_ERROR_MESSAGE: "Validation failed";
96
+ /** Default HTTP status when validation fails */
97
+ declare const DEFAULT_VALIDATION_STATUS: 400;
98
+
99
+ export { DEFAULT_VALIDATION_ERROR_MESSAGE, DEFAULT_VALIDATION_STATUS, type InferValidated, type ValidateOptions, type ValidationErrorBody, type ValidationIssue, type ValidationSchemas, createValidator, mapZodErrorToResponse, validate };
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ // src/constants.ts
2
+ var DEFAULT_VALIDATION_ERROR_MESSAGE = "Validation failed";
3
+ var DEFAULT_VALIDATION_STATUS = 400;
4
+
5
+ // src/middleware/error-mapper.ts
6
+ function issuePath(issue) {
7
+ return issue.path.map(String).join(".") || "(root)";
8
+ }
9
+ function mapZodErrorToResponse(err, message = DEFAULT_VALIDATION_ERROR_MESSAGE) {
10
+ return {
11
+ error: message,
12
+ issues: err.issues.map((issue) => ({
13
+ path: issuePath(issue),
14
+ message: issue.message,
15
+ code: issue.code
16
+ }))
17
+ };
18
+ }
19
+
20
+ // src/middleware/validate.ts
21
+ function parseSegment(schema, raw, errorMessage, statusCode, res) {
22
+ const result = schema.safeParse(raw);
23
+ if (result.success) {
24
+ return { ok: true, data: result.data };
25
+ }
26
+ res.status(statusCode).json(mapZodErrorToResponse(result.error, errorMessage));
27
+ return { ok: false };
28
+ }
29
+ function validate(schemas) {
30
+ const keys = ["body", "query", "params"];
31
+ const hasAny = keys.some((k) => schemas[k] !== void 0);
32
+ if (!hasAny) {
33
+ throw new Error(
34
+ "express-zod-routes: validate() requires at least one of body, query, or params"
35
+ );
36
+ }
37
+ const statusCode = schemas.statusCode ?? DEFAULT_VALIDATION_STATUS;
38
+ const errorMessage = schemas.errorMessage ?? DEFAULT_VALIDATION_ERROR_MESSAGE;
39
+ return (req, res, next) => {
40
+ try {
41
+ const prev = req.validated ?? {};
42
+ const nextValidated = { ...prev };
43
+ if (schemas.body) {
44
+ const out = parseSegment(schemas.body, req.body, errorMessage, statusCode, res);
45
+ if (!out.ok) return;
46
+ nextValidated.body = out.data;
47
+ }
48
+ if (schemas.query) {
49
+ const out = parseSegment(schemas.query, req.query, errorMessage, statusCode, res);
50
+ if (!out.ok) return;
51
+ nextValidated.query = out.data;
52
+ }
53
+ if (schemas.params) {
54
+ const out = parseSegment(schemas.params, req.params, errorMessage, statusCode, res);
55
+ if (!out.ok) return;
56
+ nextValidated.params = out.data;
57
+ }
58
+ req.validated = nextValidated;
59
+ next();
60
+ } catch (e) {
61
+ next(e);
62
+ }
63
+ };
64
+ }
65
+ var createValidator = validate;
66
+
67
+ export { DEFAULT_VALIDATION_ERROR_MESSAGE, DEFAULT_VALIDATION_STATUS, createValidator, mapZodErrorToResponse, validate };
68
+ //# sourceMappingURL=index.js.map
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/middleware/error-mapper.ts","../src/middleware/validate.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** Default message for validation error responses */\nexport const DEFAULT_VALIDATION_ERROR_MESSAGE = 'Validation failed' as const;\n\n/** Default HTTP status when validation fails */\nexport const DEFAULT_VALIDATION_STATUS = 400 as const;\n","import type { ZodError, ZodIssue } from 'zod';\nimport { DEFAULT_VALIDATION_ERROR_MESSAGE } from '../constants.js';\n\n/** One validation issue in the API response */\nexport interface ValidationIssue {\n /** Dot-separated path (e.g. `user.email`) */\n path: string;\n message: string;\n code: ZodIssue['code'];\n}\n\n/** Stable JSON body for HTTP 400 validation errors */\nexport interface ValidationErrorBody {\n error: string;\n issues: ValidationIssue[];\n}\n\nfunction issuePath(issue: ZodIssue): string {\n return issue.path.map(String).join('.') || '(root)';\n}\n\n/**\n * Maps a Zod error to the package’s standard error payload (for custom error middleware).\n */\nexport function mapZodErrorToResponse(\n err: ZodError,\n message: string = DEFAULT_VALIDATION_ERROR_MESSAGE\n): ValidationErrorBody {\n return {\n error: message,\n issues: err.issues.map((issue) => ({\n path: issuePath(issue),\n message: issue.message,\n code: issue.code,\n })),\n };\n}\n","import type { NextFunction, Request, RequestHandler, Response } from 'express';\nimport type { ZodTypeAny } from 'zod';\nimport { DEFAULT_VALIDATION_ERROR_MESSAGE, DEFAULT_VALIDATION_STATUS } from '../constants.js';\nimport type { ValidationSchemas } from '../types/schemas.js';\nimport { mapZodErrorToResponse } from './error-mapper.js';\n\nexport interface ValidateOptions extends ValidationSchemas {\n /** HTTP status when validation fails (default: 400) */\n statusCode?: number;\n /** Override default error message in JSON body */\n errorMessage?: string;\n}\n\nfunction parseSegment(\n schema: ZodTypeAny,\n raw: unknown,\n errorMessage: string,\n statusCode: number,\n res: Response\n): { ok: true; data: unknown } | { ok: false } {\n const result = schema.safeParse(raw);\n if (result.success) {\n return { ok: true, data: result.data };\n }\n res.status(statusCode).json(mapZodErrorToResponse(result.error, errorMessage));\n return { ok: false };\n}\n\n/**\n * Express middleware: validates `body`, `query`, and/or `params` with Zod.\n * On success, merges results into `req.validated`. On failure, sends JSON 400 and does not call `next`.\n *\n * @example\n * ```ts\n * app.post(\n * '/users',\n * express.json(),\n * validate({ body: z.object({ name: z.string().min(1) }) }),\n * (req, res) => res.json(req.validated!.body)\n * );\n * ```\n */\nexport function validate(schemas: ValidateOptions): RequestHandler {\n const keys = ['body', 'query', 'params'] as const;\n const hasAny = keys.some((k) => schemas[k] !== undefined);\n if (!hasAny) {\n throw new Error(\n 'express-zod-routes: validate() requires at least one of body, query, or params'\n );\n }\n\n const statusCode = schemas.statusCode ?? DEFAULT_VALIDATION_STATUS;\n const errorMessage = schemas.errorMessage ?? DEFAULT_VALIDATION_ERROR_MESSAGE;\n\n return (req: Request, res: Response, next: NextFunction) => {\n try {\n const prev = req.validated ?? {};\n const nextValidated: NonNullable<Request['validated']> = { ...prev };\n\n if (schemas.body) {\n const out = parseSegment(schemas.body, req.body, errorMessage, statusCode, res);\n if (!out.ok) return;\n nextValidated.body = out.data;\n }\n if (schemas.query) {\n const out = parseSegment(schemas.query, req.query, errorMessage, statusCode, res);\n if (!out.ok) return;\n nextValidated.query = out.data;\n }\n if (schemas.params) {\n const out = parseSegment(schemas.params, req.params, errorMessage, statusCode, res);\n if (!out.ok) return;\n nextValidated.params = out.data;\n }\n\n req.validated = nextValidated;\n next();\n } catch (e) {\n next(e);\n }\n };\n}\n\n/** Alias for {@link validate} */\nexport const createValidator = validate;\n"]}
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "express-zod-routes",
3
+ "version": "1.0.0",
4
+ "description": "Express middleware to validate body, query, and params with Zod — typed req.validated and consistent 400 errors",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "sideEffects": [
27
+ "./src/global-augment.ts"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "lint": "eslint .",
34
+ "format": "prettier --write .",
35
+ "format:check": "prettier --check .",
36
+ "typecheck": "tsc --noEmit",
37
+ "prepublishOnly": "npm run build && npm run test"
38
+ },
39
+ "keywords": [
40
+ "express",
41
+ "zod",
42
+ "validation",
43
+ "middleware",
44
+ "typescript",
45
+ "mern"
46
+ ],
47
+ "author": "",
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git@github.com:adriangrahldev/express-zod-routes.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/adriangrahldev/express-zod-routes/issues"
55
+ },
56
+ "homepage": "https://github.com/adriangrahldev/express-zod-routes#readme",
57
+ "engines": {
58
+ "node": ">=18"
59
+ },
60
+ "peerDependencies": {
61
+ "express": "^4.18.0 || ^5.0.0",
62
+ "zod": "^3.22.0"
63
+ },
64
+ "devDependencies": {
65
+ "@types/express": "^5.0.0",
66
+ "@types/node": "^22.10.0",
67
+ "@types/supertest": "^6.0.2",
68
+ "@eslint/js": "^9.16.0",
69
+ "eslint": "^9.16.0",
70
+ "express": "^4.21.0",
71
+ "prettier": "^3.4.2",
72
+ "supertest": "^7.0.0",
73
+ "tsup": "^8.3.5",
74
+ "typescript": "^5.7.2",
75
+ "typescript-eslint": "^8.18.0",
76
+ "vitest": "^2.1.8",
77
+ "zod": "^3.24.1"
78
+ }
79
+ }