@devmunna/agent-skillkit 0.1.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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/bin/ai-skills.js +5 -0
  4. package/dist/cli/commands/add.d.ts +2 -0
  5. package/dist/cli/commands/add.d.ts.map +1 -0
  6. package/dist/cli/commands/add.js +66 -0
  7. package/dist/cli/commands/add.js.map +1 -0
  8. package/dist/cli/commands/doctor.d.ts +2 -0
  9. package/dist/cli/commands/doctor.d.ts.map +1 -0
  10. package/dist/cli/commands/doctor.js +33 -0
  11. package/dist/cli/commands/doctor.js.map +1 -0
  12. package/dist/cli/commands/init.d.ts +10 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +145 -0
  15. package/dist/cli/commands/init.js.map +1 -0
  16. package/dist/cli/commands/list.d.ts +5 -0
  17. package/dist/cli/commands/list.d.ts.map +1 -0
  18. package/dist/cli/commands/list.js +55 -0
  19. package/dist/cli/commands/list.js.map +1 -0
  20. package/dist/cli/commands/update.d.ts +2 -0
  21. package/dist/cli/commands/update.d.ts.map +1 -0
  22. package/dist/cli/commands/update.js +49 -0
  23. package/dist/cli/commands/update.js.map +1 -0
  24. package/dist/cli/commands/validate.d.ts +2 -0
  25. package/dist/cli/commands/validate.d.ts.map +1 -0
  26. package/dist/cli/commands/validate.js +22 -0
  27. package/dist/cli/commands/validate.js.map +1 -0
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -0
  30. package/dist/cli/index.js +49 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/prompts/agent-selector.d.ts +3 -0
  33. package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
  34. package/dist/cli/prompts/agent-selector.js +23 -0
  35. package/dist/cli/prompts/agent-selector.js.map +1 -0
  36. package/dist/cli/prompts/stack-selector.d.ts +3 -0
  37. package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
  38. package/dist/cli/prompts/stack-selector.js +60 -0
  39. package/dist/cli/prompts/stack-selector.js.map +1 -0
  40. package/dist/core/config-manager.d.ts +20 -0
  41. package/dist/core/config-manager.d.ts.map +1 -0
  42. package/dist/core/config-manager.js +107 -0
  43. package/dist/core/config-manager.js.map +1 -0
  44. package/dist/core/detector.d.ts +3 -0
  45. package/dist/core/detector.d.ts.map +1 -0
  46. package/dist/core/detector.js +50 -0
  47. package/dist/core/detector.js.map +1 -0
  48. package/dist/core/doctor.d.ts +12 -0
  49. package/dist/core/doctor.d.ts.map +1 -0
  50. package/dist/core/doctor.js +102 -0
  51. package/dist/core/doctor.js.map +1 -0
  52. package/dist/core/skill-registry.d.ts +11 -0
  53. package/dist/core/skill-registry.d.ts.map +1 -0
  54. package/dist/core/skill-registry.js +174 -0
  55. package/dist/core/skill-registry.js.map +1 -0
  56. package/dist/core/skill-resolver.d.ts +3 -0
  57. package/dist/core/skill-resolver.d.ts.map +1 -0
  58. package/dist/core/skill-resolver.js +36 -0
  59. package/dist/core/skill-resolver.js.map +1 -0
  60. package/dist/core/validator.d.ts +13 -0
  61. package/dist/core/validator.d.ts.map +1 -0
  62. package/dist/core/validator.js +99 -0
  63. package/dist/core/validator.js.map +1 -0
  64. package/dist/generators/agent-installer.d.ts +5 -0
  65. package/dist/generators/agent-installer.d.ts.map +1 -0
  66. package/dist/generators/agent-installer.js +20 -0
  67. package/dist/generators/agent-installer.js.map +1 -0
  68. package/dist/generators/agents-md.d.ts +3 -0
  69. package/dist/generators/agents-md.d.ts.map +1 -0
  70. package/dist/generators/agents-md.js +70 -0
  71. package/dist/generators/agents-md.js.map +1 -0
  72. package/dist/generators/claude-md.d.ts +3 -0
  73. package/dist/generators/claude-md.d.ts.map +1 -0
  74. package/dist/generators/claude-md.js +47 -0
  75. package/dist/generators/claude-md.js.map +1 -0
  76. package/dist/generators/skill-generator.d.ts +5 -0
  77. package/dist/generators/skill-generator.d.ts.map +1 -0
  78. package/dist/generators/skill-generator.js +34 -0
  79. package/dist/generators/skill-generator.js.map +1 -0
  80. package/dist/generators/workflows.d.ts +3 -0
  81. package/dist/generators/workflows.d.ts.map +1 -0
  82. package/dist/generators/workflows.js +57 -0
  83. package/dist/generators/workflows.js.map +1 -0
  84. package/dist/index.d.ts +13 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +13 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/types/index.d.ts +55 -0
  89. package/dist/types/index.d.ts.map +1 -0
  90. package/dist/types/index.js +2 -0
  91. package/dist/types/index.js.map +1 -0
  92. package/dist/utils/file-utils.d.ts +12 -0
  93. package/dist/utils/file-utils.d.ts.map +1 -0
  94. package/dist/utils/file-utils.js +39 -0
  95. package/dist/utils/file-utils.js.map +1 -0
  96. package/dist/utils/logger.d.ts +10 -0
  97. package/dist/utils/logger.d.ts.map +1 -0
  98. package/dist/utils/logger.js +11 -0
  99. package/dist/utils/logger.js.map +1 -0
  100. package/package.json +73 -0
  101. package/skills/clean-architecture/SKILL.md +324 -0
  102. package/skills/express-mvc-prisma/SKILL.md +168 -0
  103. package/skills/express-mvc-prisma/references/auth.md +190 -0
  104. package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
  105. package/skills/express-mvc-prisma/references/error-handling.md +121 -0
  106. package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
  107. package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
  108. package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
  109. package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
  110. package/skills/fastify-rest/SKILL.md +287 -0
  111. package/skills/mongoose-odm/SKILL.md +281 -0
  112. package/skills/nextjs-fullstack/SKILL.md +328 -0
  113. package/skills/nextjs-fullstack/references/auth.md +270 -0
  114. package/skills/nextjs-fullstack/references/caching.md +157 -0
  115. package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
  116. package/skills/nextjs-fullstack/references/server-actions.md +214 -0
  117. package/skills/nextjs-fullstack/references/server-components.md +190 -0
  118. package/skills/node-base/SKILL.md +139 -0
  119. package/skills/prisma-orm/SKILL.md +334 -0
  120. package/skills/react-feature-arch/SKILL.md +208 -0
  121. package/skills/react-feature-arch/references/api-layer.md +110 -0
  122. package/skills/react-feature-arch/references/components.md +192 -0
  123. package/skills/react-feature-arch/references/data-fetching.md +198 -0
  124. package/skills/react-feature-arch/references/forms.md +194 -0
  125. package/skills/react-feature-arch/references/routing.md +148 -0
  126. package/skills/react-feature-arch/references/state-management.md +107 -0
  127. package/skills/tailwind-css/SKILL.md +236 -0
  128. package/skills/tailwind-css/references/components.md +340 -0
  129. package/skills/tailwind-css/references/design-tokens.md +230 -0
  130. package/skills/tailwind-css/references/patterns.md +375 -0
  131. package/skills/tailwind-css/references/setup.md +165 -0
  132. package/skills/zod-validation/SKILL.md +267 -0
@@ -0,0 +1,253 @@
1
+ # Module Scaffold Reference
2
+
3
+ Complete 4-file template. Replace `user`/`User`/`users` with your resource name.
4
+
5
+ ---
6
+
7
+ ## {module}.validation.js
8
+
9
+ Validates `body`, `params`, and `query` using Zod v4. One schema per endpoint.
10
+
11
+ ```js
12
+ import { z } from 'zod';
13
+
14
+ // Body schema for POST /users
15
+ export const createUserSchema = z.object({
16
+ body: z.object({
17
+ name: z.string().min(2, 'Name min 2 chars').max(100),
18
+ email: z.string().email('Invalid email'),
19
+ password: z.string().min(8, 'Password min 8 chars'),
20
+ role: z.enum(['USER', 'ADMIN']).default('USER'),
21
+ }).strict(),
22
+ });
23
+
24
+ // Body + params for PATCH /users/:id
25
+ export const updateUserSchema = z.object({
26
+ params: z.object({ id: z.string().uuid('Invalid ID') }),
27
+ body: z.object({
28
+ name: z.string().min(2).max(100).optional(),
29
+ email: z.string().email().optional(),
30
+ role: z.enum(['USER', 'ADMIN']).optional(),
31
+ }).strict(),
32
+ });
33
+
34
+ // Params only for GET /users/:id, DELETE /users/:id
35
+ export const userIdSchema = z.object({
36
+ params: z.object({ id: z.string().uuid('Invalid ID') }),
37
+ });
38
+
39
+ // Query for GET /users?page=1&limit=10&search=
40
+ export const listUsersSchema = z.object({
41
+ query: z.object({
42
+ page: z.coerce.number().int().positive().default(1),
43
+ limit: z.coerce.number().int().positive().max(100).default(10),
44
+ search: z.string().optional(),
45
+ role: z.enum(['USER', 'ADMIN']).optional(),
46
+ }),
47
+ });
48
+
49
+ /** @typedef {import('zod').infer<typeof createUserSchema>['body']} CreateUserDto */
50
+ /** @typedef {import('zod').infer<typeof updateUserSchema>['body']} UpdateUserDto */
51
+ ```
52
+
53
+ ---
54
+
55
+ ## {module}.service.js
56
+
57
+ Business logic + all Prisma calls. Throw `AppError` for expected failures.
58
+ Never import `res` or `req` here.
59
+
60
+ ```js
61
+ import { Prisma } from '@prisma/client';
62
+ import prisma from '../../config/db.js';
63
+ import { AppError } from '../../utils/AppError.js';
64
+ import bcrypt from 'bcrypt';
65
+
66
+ export const userService = {
67
+
68
+ async getAll({ page = 1, limit = 10, search, role } = {}) {
69
+ const skip = (page - 1) * limit;
70
+ const where = {};
71
+ if (role) where.role = role;
72
+ if (search) where.OR = [
73
+ { name: { contains: search, mode: 'insensitive' } },
74
+ { email: { contains: search, mode: 'insensitive' } },
75
+ ];
76
+
77
+ const [data, total] = await prisma.$transaction([
78
+ prisma.user.findMany({
79
+ where,
80
+ skip,
81
+ take: limit,
82
+ orderBy: { createdAt: 'desc' },
83
+ select: { id: true, name: true, email: true, role: true, createdAt: true },
84
+ }),
85
+ prisma.user.count({ where }),
86
+ ]);
87
+
88
+ return { data, total, page, limit };
89
+ },
90
+
91
+ async getById(id) {
92
+ const user = await prisma.user.findUnique({
93
+ where: { id },
94
+ select: { id: true, name: true, email: true, role: true, createdAt: true },
95
+ });
96
+ if (!user) throw new AppError('User not found', 404);
97
+ return user;
98
+ },
99
+
100
+ async create(data) {
101
+ const exists = await prisma.user.findUnique({ where: { email: data.email } });
102
+ if (exists) throw new AppError('Email already registered', 409);
103
+
104
+ const passwordHash = await bcrypt.hash(data.password, 12);
105
+
106
+ try {
107
+ const user = await prisma.user.create({
108
+ data: { ...data, password: passwordHash },
109
+ select: { id: true, name: true, email: true, role: true, createdAt: true },
110
+ });
111
+ return user;
112
+ } catch (err) {
113
+ if (err instanceof Prisma.PrismaClientKnownRequestError) {
114
+ if (err.code === 'P2002') throw new AppError('Email already registered', 409);
115
+ }
116
+ throw err;
117
+ }
118
+ },
119
+
120
+ async update(id, data) {
121
+ try {
122
+ return await prisma.user.update({
123
+ where: { id },
124
+ data,
125
+ select: { id: true, name: true, email: true, role: true, updatedAt: true },
126
+ });
127
+ } catch (err) {
128
+ if (err instanceof Prisma.PrismaClientKnownRequestError) {
129
+ if (err.code === 'P2025') throw new AppError('User not found', 404);
130
+ }
131
+ throw err;
132
+ }
133
+ },
134
+
135
+ async remove(id) {
136
+ try {
137
+ await prisma.user.delete({ where: { id } });
138
+ } catch (err) {
139
+ if (err instanceof Prisma.PrismaClientKnownRequestError) {
140
+ if (err.code === 'P2025') throw new AppError('User not found', 404);
141
+ }
142
+ throw err;
143
+ }
144
+ },
145
+ };
146
+ ```
147
+
148
+ ---
149
+
150
+ ## {module}.controller.js
151
+
152
+ HTTP layer only. No business logic, no Prisma, no try/catch.
153
+ Express v5 auto-forwards async errors to the global error handler.
154
+
155
+ ```js
156
+ import { userService } from './user.service.js';
157
+ import { sendSuccess } from '../../utils/response.js';
158
+
159
+ export const userController = {
160
+
161
+ getAll: async (req, res) => {
162
+ const result = await userService.getAll(req.query);
163
+ sendSuccess(res, 'Users fetched', result);
164
+ },
165
+
166
+ getById: async (req, res) => {
167
+ const user = await userService.getById(req.params.id);
168
+ sendSuccess(res, 'User fetched', user);
169
+ },
170
+
171
+ create: async (req, res) => {
172
+ const user = await userService.create(req.body);
173
+ sendSuccess(res, 'User created', user, 201);
174
+ },
175
+
176
+ update: async (req, res) => {
177
+ const user = await userService.update(req.params.id, req.body);
178
+ sendSuccess(res, 'User updated', user);
179
+ },
180
+
181
+ remove: async (req, res) => {
182
+ await userService.remove(req.params.id);
183
+ sendSuccess(res, 'User deleted');
184
+ },
185
+ };
186
+ ```
187
+
188
+ ---
189
+
190
+ ## {module}.routes.js
191
+
192
+ Wires middleware → controller. Route order matters: specific before dynamic.
193
+
194
+ ```js
195
+ import { Router } from 'express';
196
+ import { validate } from '../../middlewares/validate.middleware.js';
197
+ import { authenticate, requireRole } from '../../middlewares/auth.middleware.js';
198
+ import {
199
+ createUserSchema, updateUserSchema,
200
+ userIdSchema, listUsersSchema,
201
+ } from './user.validation.js';
202
+ import { userController } from './user.controller.js';
203
+
204
+ const router = Router();
205
+
206
+ router.get( '/', authenticate, validate(listUsersSchema), userController.getAll);
207
+ router.get( '/:id', authenticate, validate(userIdSchema), userController.getById);
208
+ router.post( '/', authenticate, validate(createUserSchema), userController.create);
209
+ router.patch('/:id', authenticate, validate(updateUserSchema), userController.update);
210
+ router.delete('/:id',authenticate, validate(userIdSchema), userController.remove);
211
+
212
+ export default router;
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Register in src/routes/index.js
218
+
219
+ ```js
220
+ import userRouter from '../modules/user/user.routes.js';
221
+
222
+ router.use('/users', userRouter);
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Pagination Response Shape
228
+
229
+ ```json
230
+ {
231
+ "success": true,
232
+ "message": "Users fetched",
233
+ "data": [...],
234
+ "meta": {
235
+ "total": 85,
236
+ "page": 2,
237
+ "limit": 10,
238
+ "totalPages": 9,
239
+ "hasNext": true,
240
+ "hasPrev": true
241
+ }
242
+ }
243
+ ```
244
+
245
+ Single item:
246
+ ```json
247
+ { "success": true, "message": "User fetched", "data": { "id": "...", "name": "..." } }
248
+ ```
249
+
250
+ Deleted / no data:
251
+ ```json
252
+ { "success": true, "message": "User deleted" }
253
+ ```
@@ -0,0 +1,97 @@
1
+ # Prisma Setup Reference
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ npm install @prisma/client
7
+ npm install -D prisma
8
+ npx prisma init --datasource-provider postgresql
9
+ ```
10
+
11
+ ---
12
+
13
+ ## prisma/schema.prisma — Base Template
14
+
15
+ ```prisma
16
+ generator client {
17
+ provider = "prisma-client-js"
18
+ }
19
+
20
+ datasource db {
21
+ provider = "postgresql"
22
+ url = env("DATABASE_URL")
23
+ }
24
+
25
+ model User {
26
+ id String @id @default(uuid())
27
+ email String @unique
28
+ name String
29
+ password String
30
+ role Role @default(USER)
31
+ createdAt DateTime @default(now())
32
+ updatedAt DateTime @updatedAt
33
+
34
+ @@map("users")
35
+ }
36
+
37
+ enum Role {
38
+ USER
39
+ ADMIN
40
+ }
41
+ ```
42
+
43
+ **Conventions:**
44
+ - Always use `uuid()` for IDs (not auto-increment)
45
+ - Always add `createdAt` + `updatedAt` to every model
46
+ - Use `@@map("snake_case_table_name")` to keep DB column names clean
47
+ - Use enums for fixed value sets (role, status, type)
48
+
49
+ ---
50
+
51
+ ## Migration Commands
52
+
53
+ ```bash
54
+ npx prisma migrate dev --name init # Create + apply migration (dev)
55
+ npx prisma migrate deploy # Apply migrations in production
56
+ npx prisma migrate reset # Reset DB (dev only — destroys data)
57
+ npx prisma generate # Regenerate client after schema change
58
+ npx prisma studio # Open visual DB browser
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Handling Prisma Errors in Repository
64
+
65
+ ```js
66
+ import { Prisma } from '@prisma/client';
67
+
68
+ try {
69
+ // ...prisma call
70
+ } catch (error) {
71
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
72
+ switch (error.code) {
73
+ case 'P2002': throw new AppError('Duplicate entry: ' + error.meta?.target, 409);
74
+ case 'P2025': throw new AppError('Record not found', 404);
75
+ case 'P2003': throw new AppError('Related record not found', 400);
76
+ case 'P2014': throw new AppError('Relation violation', 400);
77
+ default: throw error;
78
+ }
79
+ }
80
+ throw error;
81
+ }
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Transaction Example
87
+
88
+ ```js
89
+ const result = await prisma.$transaction(async (tx) => {
90
+ const order = await tx.order.create({ data: orderData });
91
+ await tx.product.update({
92
+ where: { id: orderData.productId },
93
+ data: { stock: { decrement: orderData.quantity } },
94
+ });
95
+ return order;
96
+ });
97
+ ```
@@ -0,0 +1,157 @@
1
+ # Response Helpers Reference
2
+
3
+ ---
4
+
5
+ ## src/utils/response.js
6
+
7
+ ```js
8
+ /**
9
+ * Send a successful JSON response.
10
+ * If data has { data, total, page, limit } shape → auto-adds meta pagination block.
11
+ */
12
+ export const sendSuccess = (res, message, data = null, statusCode = 200) => {
13
+ const body = { success: true, message };
14
+
15
+ if (data !== null) {
16
+ if (isPaginated(data)) {
17
+ body.data = data.data;
18
+ body.meta = buildMeta(data);
19
+ } else {
20
+ body.data = data;
21
+ }
22
+ }
23
+
24
+ return res.status(statusCode).json(body);
25
+ };
26
+
27
+ const isPaginated = (d) =>
28
+ d !== null &&
29
+ typeof d === 'object' &&
30
+ 'data' in d &&
31
+ 'total' in d &&
32
+ 'page' in d &&
33
+ 'limit' in d;
34
+
35
+ const buildMeta = ({ total, page, limit }) => ({
36
+ total,
37
+ page,
38
+ limit,
39
+ totalPages: Math.ceil(total / limit),
40
+ hasNext: page * limit < total,
41
+ hasPrev: page > 1,
42
+ });
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Usage in controllers
48
+
49
+ ```js
50
+ // List (auto-paginates)
51
+ sendSuccess(res, 'Users fetched', { data: users, total, page, limit });
52
+
53
+ // Single item
54
+ sendSuccess(res, 'User fetched', user);
55
+
56
+ // Created
57
+ sendSuccess(res, 'User created', user, 201);
58
+
59
+ // Deleted / no data
60
+ sendSuccess(res, 'User deleted');
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Response Shapes
66
+
67
+ ### Single item
68
+ ```json
69
+ {
70
+ "success": true,
71
+ "message": "User fetched",
72
+ "data": { "id": "uuid", "name": "Alice", "email": "alice@example.com" }
73
+ }
74
+ ```
75
+
76
+ ### Paginated list
77
+ ```json
78
+ {
79
+ "success": true,
80
+ "message": "Users fetched",
81
+ "data": [...],
82
+ "meta": {
83
+ "total": 85,
84
+ "page": 2,
85
+ "limit": 10,
86
+ "totalPages": 9,
87
+ "hasNext": true,
88
+ "hasPrev": true
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### Created (201)
94
+ ```json
95
+ { "success": true, "message": "User created", "data": { "id": "...", "name": "..." } }
96
+ ```
97
+
98
+ ### No data (delete, void operations)
99
+ ```json
100
+ { "success": true, "message": "User deleted" }
101
+ ```
102
+
103
+ ### Error (from errorHandler)
104
+ ```json
105
+ {
106
+ "success": false,
107
+ "message": "User not found"
108
+ }
109
+ ```
110
+
111
+ ### Validation error (from validate middleware)
112
+ ```json
113
+ {
114
+ "success": false,
115
+ "message": "Invalid email",
116
+ "errors": [
117
+ { "field": "body.email", "message": "Invalid email" }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Pagination in service layer
125
+
126
+ ```js
127
+ async getAll({ page = 1, limit = 10, search } = {}) {
128
+ const skip = (page - 1) * limit;
129
+ const where = search
130
+ ? { name: { contains: search, mode: 'insensitive' } }
131
+ : {};
132
+
133
+ const [data, total] = await prisma.$transaction([
134
+ prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' } }),
135
+ prisma.user.count({ where }),
136
+ ]);
137
+
138
+ return { data, total, page, limit }; // sendSuccess auto-detects this shape
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## HTTP Status Codes
145
+
146
+ | Code | Use case |
147
+ |------|---------|
148
+ | 200 | OK — GET, PATCH, DELETE success |
149
+ | 201 | Created — POST success |
150
+ | 400 | Bad Request — business logic error |
151
+ | 401 | Unauthorized — missing or invalid token |
152
+ | 403 | Forbidden — authenticated, insufficient role |
153
+ | 404 | Not Found |
154
+ | 409 | Conflict — duplicate entry |
155
+ | 422 | Unprocessable — validation error |
156
+ | 429 | Too Many Requests — rate limit |
157
+ | 500 | Internal Server Error |
@@ -0,0 +1,157 @@
1
+ # Zod v4 Validation Reference
2
+
3
+ ---
4
+
5
+ ## validate.middleware.js
6
+
7
+ Validates `body`, `params`, and `query` in one call. Uses Zod v4 `.issues` API.
8
+
9
+ ```js
10
+ import { AppError } from '../utils/AppError.js';
11
+
12
+ export const validate = (schema) => (req, res, next) => {
13
+ const result = schema.safeParse({
14
+ body: req.body,
15
+ params: req.params,
16
+ query: req.query,
17
+ });
18
+
19
+ if (!result.success) {
20
+ // Zod v4: use .issues (not .flatten())
21
+ const errors = result.error.issues.map((i) => ({
22
+ field: i.path.join('.'),
23
+ message: i.message,
24
+ }));
25
+ return next(new AppError(errors[0]?.message || 'Validation failed', 422, errors));
26
+ }
27
+
28
+ // Replace with parsed/coerced/defaulted values
29
+ if (result.data.body) req.body = result.data.body;
30
+ if (result.data.params) req.params = result.data.params;
31
+ if (result.data.query) req.query = result.data.query;
32
+
33
+ next();
34
+ };
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Schema Structure
40
+
41
+ Always use `z.object({ body?, params?, query? })` for the validate middleware:
42
+
43
+ ```js
44
+ import { z } from 'zod';
45
+
46
+ // POST body only
47
+ export const createSchema = z.object({
48
+ body: z.object({ name: z.string().min(1), email: z.string().email() }).strict(),
49
+ });
50
+
51
+ // Params + body
52
+ export const updateSchema = z.object({
53
+ params: z.object({ id: z.string().uuid() }),
54
+ body: z.object({
55
+ name: z.string().min(1).optional(),
56
+ email: z.string().email().optional(),
57
+ }).strict(),
58
+ });
59
+
60
+ // Params only
61
+ export const idSchema = z.object({
62
+ params: z.object({ id: z.string().uuid() }),
63
+ });
64
+
65
+ // Query only (list / paginate)
66
+ export const listSchema = z.object({
67
+ query: z.object({
68
+ page: z.coerce.number().int().positive().default(1),
69
+ limit: z.coerce.number().int().positive().max(100).default(10),
70
+ search: z.string().optional(),
71
+ }),
72
+ });
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Zod v4 Validator Reference
78
+
79
+ ### Strings
80
+ ```js
81
+ z.string().min(1, 'Required').max(255)
82
+ z.string().email()
83
+ z.string().uuid()
84
+ z.string().url()
85
+ z.string().regex(/^\d{6}$/, 'Must be 6 digits')
86
+ z.string().trim().toLowerCase() // transform on parse
87
+ z.string().nonempty() // min(1) shorthand
88
+ ```
89
+
90
+ ### Numbers
91
+ ```js
92
+ z.number().int().positive()
93
+ z.number().min(0).max(100)
94
+ z.coerce.number().int().positive() // always use coerce for query params
95
+ ```
96
+
97
+ ### Optional / Nullable
98
+ ```js
99
+ z.string().optional() // string | undefined
100
+ z.string().nullable() // string | null
101
+ z.string().nullish() // string | null | undefined
102
+ z.string().default('x') // fallback when undefined
103
+ ```
104
+
105
+ ### Enums
106
+ ```js
107
+ z.enum(['USER', 'ADMIN', 'MANAGER'])
108
+ ```
109
+
110
+ ### Objects
111
+ ```js
112
+ z.object({ name: z.string() }).strict() // fail on unknown keys
113
+ z.object({ name: z.string() }).partial() // all optional
114
+ z.object({ name: z.string() }).passthrough() // allow extra keys
115
+ ```
116
+
117
+ ### Arrays
118
+ ```js
119
+ z.array(z.string()).min(1).max(50)
120
+ z.array(z.string().uuid())
121
+ ```
122
+
123
+ ### Dates
124
+ ```js
125
+ z.coerce.date() // "2024-01-01" → Date
126
+ z.date().min(new Date()) // must be in the future
127
+ ```
128
+
129
+ ### Transformations
130
+ ```js
131
+ z.string().transform((v) => v.trim().toLowerCase())
132
+ z.preprocess((v) => Number(v), z.number())
133
+ ```
134
+
135
+ ### Cross-field validation
136
+ ```js
137
+ z.object({
138
+ password: z.string().min(8),
139
+ confirmPassword: z.string(),
140
+ }).refine((d) => d.password === d.confirmPassword, {
141
+ message: 'Passwords do not match',
142
+ path: ['confirmPassword'],
143
+ });
144
+ ```
145
+
146
+ ### Zod v4 error format in validate middleware response
147
+
148
+ ```json
149
+ {
150
+ "success": false,
151
+ "message": "Invalid email",
152
+ "errors": [
153
+ { "field": "body.email", "message": "Invalid email" },
154
+ { "field": "body.name", "message": "Name min 2 chars" }
155
+ ]
156
+ }
157
+ ```