@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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/ai-skills.js +5 -0
- package/dist/cli/commands/add.d.ts +2 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +66 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +33 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +55 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +22 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts/agent-selector.d.ts +3 -0
- package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
- package/dist/cli/prompts/agent-selector.js +23 -0
- package/dist/cli/prompts/agent-selector.js.map +1 -0
- package/dist/cli/prompts/stack-selector.d.ts +3 -0
- package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
- package/dist/cli/prompts/stack-selector.js +60 -0
- package/dist/cli/prompts/stack-selector.js.map +1 -0
- package/dist/core/config-manager.d.ts +20 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +107 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/detector.d.ts +3 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +50 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/doctor.d.ts +12 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +102 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/skill-registry.d.ts +11 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +174 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-resolver.d.ts +3 -0
- package/dist/core/skill-resolver.d.ts.map +1 -0
- package/dist/core/skill-resolver.js +36 -0
- package/dist/core/skill-resolver.js.map +1 -0
- package/dist/core/validator.d.ts +13 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +99 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/generators/agent-installer.d.ts +5 -0
- package/dist/generators/agent-installer.d.ts.map +1 -0
- package/dist/generators/agent-installer.js +20 -0
- package/dist/generators/agent-installer.js.map +1 -0
- package/dist/generators/agents-md.d.ts +3 -0
- package/dist/generators/agents-md.d.ts.map +1 -0
- package/dist/generators/agents-md.js +70 -0
- package/dist/generators/agents-md.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +47 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/skill-generator.d.ts +5 -0
- package/dist/generators/skill-generator.d.ts.map +1 -0
- package/dist/generators/skill-generator.js +34 -0
- package/dist/generators/skill-generator.js.map +1 -0
- package/dist/generators/workflows.d.ts +3 -0
- package/dist/generators/workflows.d.ts.map +1 -0
- package/dist/generators/workflows.js +57 -0
- package/dist/generators/workflows.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +12 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +39 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +73 -0
- package/skills/clean-architecture/SKILL.md +324 -0
- package/skills/express-mvc-prisma/SKILL.md +168 -0
- package/skills/express-mvc-prisma/references/auth.md +190 -0
- package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
- package/skills/express-mvc-prisma/references/error-handling.md +121 -0
- package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
- package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
- package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
- package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
- package/skills/fastify-rest/SKILL.md +287 -0
- package/skills/mongoose-odm/SKILL.md +281 -0
- package/skills/nextjs-fullstack/SKILL.md +328 -0
- package/skills/nextjs-fullstack/references/auth.md +270 -0
- package/skills/nextjs-fullstack/references/caching.md +157 -0
- package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
- package/skills/nextjs-fullstack/references/server-actions.md +214 -0
- package/skills/nextjs-fullstack/references/server-components.md +190 -0
- package/skills/node-base/SKILL.md +139 -0
- package/skills/prisma-orm/SKILL.md +334 -0
- package/skills/react-feature-arch/SKILL.md +208 -0
- package/skills/react-feature-arch/references/api-layer.md +110 -0
- package/skills/react-feature-arch/references/components.md +192 -0
- package/skills/react-feature-arch/references/data-fetching.md +198 -0
- package/skills/react-feature-arch/references/forms.md +194 -0
- package/skills/react-feature-arch/references/routing.md +148 -0
- package/skills/react-feature-arch/references/state-management.md +107 -0
- package/skills/tailwind-css/SKILL.md +236 -0
- package/skills/tailwind-css/references/components.md +340 -0
- package/skills/tailwind-css/references/design-tokens.md +230 -0
- package/skills/tailwind-css/references/patterns.md +375 -0
- package/skills/tailwind-css/references/setup.md +165 -0
- package/skills/zod-validation/SKILL.md +267 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: express-mvc-prisma
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Use this skill for any Express.js backend task: project setup, new module, route/controller/service/validation, Prisma queries, error handling, auth, middleware, or API design. Triggers: "Express API", "add a module", "create CRUD", "validate request", "set up Prisma", "JWT auth", "error handling", "Express folder structure". Always enforce the 4-file module structure and all patterns below — even when the user only asks for a single file.
|
|
6
|
+
stack: [express, prisma, postgresql, zod, jwt]
|
|
7
|
+
depends: [node-base]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Express + Prisma + Zod — Production Skill
|
|
11
|
+
|
|
12
|
+
**Version target:** Express v5 · Prisma v5 · Zod v4 · Node.js 20+ (ESM)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Tech Stack
|
|
17
|
+
|
|
18
|
+
| Layer | Package |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Framework | express v5 |
|
|
21
|
+
| ORM | @prisma/client v5 |
|
|
22
|
+
| Database | PostgreSQL |
|
|
23
|
+
| Validation | zod v4 |
|
|
24
|
+
| Auth | jsonwebtoken + bcrypt |
|
|
25
|
+
| Security | helmet + cors + express-rate-limit |
|
|
26
|
+
| Logging | morgan (dev) |
|
|
27
|
+
| Config | dotenv + Zod env validation |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Project Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
src/
|
|
35
|
+
├── config/
|
|
36
|
+
│ ├── db.js # Prisma singleton
|
|
37
|
+
│ └── env.js # Zod-validated env vars
|
|
38
|
+
├── middlewares/
|
|
39
|
+
│ ├── auth.middleware.js # JWT + role guard
|
|
40
|
+
│ ├── validate.middleware.js
|
|
41
|
+
│ └── errorHandler.js # Global error + 404
|
|
42
|
+
├── modules/ # Every feature = 4 files
|
|
43
|
+
│ └── {module}/
|
|
44
|
+
│ ├── {module}.routes.js
|
|
45
|
+
│ ├── {module}.controller.js
|
|
46
|
+
│ ├── {module}.service.js
|
|
47
|
+
│ └── {module}.validation.js
|
|
48
|
+
├── utils/
|
|
49
|
+
│ ├── AppError.js # Operational error class
|
|
50
|
+
│ └── response.js # sendSuccess / sendError
|
|
51
|
+
├── routes/
|
|
52
|
+
│ └── index.js # Mounts all module routers
|
|
53
|
+
└── app.js # Express app (no listen)
|
|
54
|
+
server.js # Entry: connect DB → listen
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Module Pattern — 4 Files (strict)
|
|
60
|
+
|
|
61
|
+
Every feature module has exactly 4 files. No more, no less.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
modules/{module}/
|
|
65
|
+
├── {module}.routes.js # Router + middleware chain
|
|
66
|
+
├── {module}.controller.js # HTTP only — call service, send response
|
|
67
|
+
├── {module}.service.js # Business logic + Prisma calls
|
|
68
|
+
└── {module}.validation.js # Zod schemas for body / params / query
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Rules:**
|
|
72
|
+
- Controllers are HTTP adapters only — no business logic, no Prisma imports
|
|
73
|
+
- Services contain all business logic and all Prisma calls
|
|
74
|
+
- Services throw `new AppError(message, statusCode)` — never `res.json` from service
|
|
75
|
+
- **Express v5**: async errors in route handlers are forwarded automatically — no `catchAsync` or `try/catch` needed in controllers
|
|
76
|
+
- Validation schemas live in `*.validation.js` — imported by routes
|
|
77
|
+
|
|
78
|
+
See `references/module-scaffold.md` for the complete 4-file template.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Express v5 — Key Change
|
|
83
|
+
|
|
84
|
+
> Express 5 natively handles rejected async route handlers. Do not wrap controllers in `catchAsync` or `try/catch`. Throw errors from services; the global error handler catches them.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
// Express v5 — async errors auto-forwarded to errorHandler
|
|
88
|
+
export const getById = async (req, res) => {
|
|
89
|
+
const user = await userService.getById(req.params.id); // throws AppError if not found
|
|
90
|
+
sendSuccess(res, 'User fetched', user);
|
|
91
|
+
};
|
|
92
|
+
// No try/catch. No catchAsync. Express v5 handles it.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Error Flow
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Service throws AppError → Controller throws (auto, Express v5) → errorHandler middleware
|
|
101
|
+
Prisma error → errorHandler maps P2002 / P2025
|
|
102
|
+
JWT error → errorHandler maps JsonWebTokenError / TokenExpiredError
|
|
103
|
+
Zod error (fallback) → errorHandler maps ZodError
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
See `references/error-handling.md`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Response Helpers — `src/utils/response.js`
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
sendSuccess(res, message, data?, statusCode = 200)
|
|
114
|
+
sendError(res, message, statusCode = 500) // used by errorHandler only
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Pagination is auto-detected: if `data` has `{ data, total, page, limit }` shape, `meta` block is added.
|
|
118
|
+
|
|
119
|
+
See `references/response-helpers.md`.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Validation — Zod v4
|
|
124
|
+
|
|
125
|
+
Schemas in `{module}.validation.js`. Validate body + params + query in one call.
|
|
126
|
+
Error accessor in v4: `result.error.issues` (not `.flatten()`).
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
// validate middleware wraps schema per request segment
|
|
130
|
+
validate(createUserSchema) // validates body, params, query from schema shape
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
See `references/zod-validation.md`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Authentication
|
|
138
|
+
|
|
139
|
+
JWT Bearer token. Auth middleware sets `req.user`.
|
|
140
|
+
Role guard: `requireRole('ADMIN')` middleware.
|
|
141
|
+
|
|
142
|
+
See `references/auth.md`.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Prisma Conventions
|
|
147
|
+
|
|
148
|
+
- UUID primary keys (`@default(uuid())`)
|
|
149
|
+
- `createdAt` + `updatedAt` on every model
|
|
150
|
+
- `@@map("snake_case_table")` on every model
|
|
151
|
+
- `@@index([foreignKey])` on every FK
|
|
152
|
+
- Catch `PrismaClientKnownRequestError` in **service layer** (not controller, not global handler)
|
|
153
|
+
|
|
154
|
+
See `references/prisma-setup.md`.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Core References
|
|
159
|
+
|
|
160
|
+
| Topic | File |
|
|
161
|
+
|---|---|
|
|
162
|
+
| Module scaffold (4-file template) | `references/module-scaffold.md` |
|
|
163
|
+
| Boilerplate (app.js, server.js, env, db) | `references/boilerplate.md` |
|
|
164
|
+
| Error handling (AppError, global handler) | `references/error-handling.md` |
|
|
165
|
+
| Auth (JWT middleware, auth module) | `references/auth.md` |
|
|
166
|
+
| Zod v4 validation patterns | `references/zod-validation.md` |
|
|
167
|
+
| Response helpers + pagination | `references/response-helpers.md` |
|
|
168
|
+
| Prisma schema, migrations, queries | `references/prisma-setup.md` |
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Auth Reference
|
|
2
|
+
|
|
3
|
+
JWT Bearer token auth. Authenticate middleware sets `req.user`. Role guard protects routes.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## src/middlewares/auth.middleware.js
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import jwt from 'jsonwebtoken';
|
|
11
|
+
import { env } from '../config/env.js';
|
|
12
|
+
import prisma from '../config/db.js';
|
|
13
|
+
import { AppError } from '../utils/AppError.js';
|
|
14
|
+
|
|
15
|
+
export const authenticate = async (req, res, next) => {
|
|
16
|
+
const header = req.headers.authorization;
|
|
17
|
+
const token = header?.startsWith('Bearer ') ? header.split(' ')[1] : null;
|
|
18
|
+
|
|
19
|
+
if (!token) return next(new AppError('No token provided', 401));
|
|
20
|
+
|
|
21
|
+
const payload = jwt.verify(token, env.JWT_SECRET); // throws → caught by errorHandler
|
|
22
|
+
|
|
23
|
+
const user = await prisma.user.findUnique({
|
|
24
|
+
where: { id: payload.sub },
|
|
25
|
+
select: { id: true, name: true, email: true, role: true, status: true },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!user || user.status !== 'ACTIVE') {
|
|
29
|
+
return next(new AppError('Account not found or inactive', 401));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
req.user = user;
|
|
33
|
+
next();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Role-based guard — use after authenticate
|
|
37
|
+
export const requireRole = (...roles) => (req, res, next) => {
|
|
38
|
+
if (!roles.includes(req.user.role)) {
|
|
39
|
+
return next(new AppError('Insufficient permissions', 403));
|
|
40
|
+
}
|
|
41
|
+
next();
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Usage in routes:
|
|
46
|
+
```js
|
|
47
|
+
router.delete('/:id', authenticate, requireRole('ADMIN'), controller.remove);
|
|
48
|
+
router.patch('/:id', authenticate, requireRole('ADMIN', 'MANAGER'), controller.update);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Auth Module — 4 Files
|
|
54
|
+
|
|
55
|
+
### auth.validation.js
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { z } from 'zod';
|
|
59
|
+
|
|
60
|
+
export const registerSchema = z.object({
|
|
61
|
+
body: z.object({
|
|
62
|
+
name: z.string().min(2).max(100),
|
|
63
|
+
email: z.string().email(),
|
|
64
|
+
password: z.string().min(8, 'Password must be ≥8 characters'),
|
|
65
|
+
}).strict(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const loginSchema = z.object({
|
|
69
|
+
body: z.object({
|
|
70
|
+
email: z.string().email(),
|
|
71
|
+
password: z.string().min(1),
|
|
72
|
+
}).strict(),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### auth.service.js
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import bcrypt from 'bcrypt';
|
|
80
|
+
import jwt from 'jsonwebtoken';
|
|
81
|
+
import prisma from '../../config/db.js';
|
|
82
|
+
import { env } from '../../config/env.js';
|
|
83
|
+
import { AppError } from '../../utils/AppError.js';
|
|
84
|
+
|
|
85
|
+
const signToken = (userId) =>
|
|
86
|
+
jwt.sign({ sub: userId }, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN });
|
|
87
|
+
|
|
88
|
+
export const authService = {
|
|
89
|
+
|
|
90
|
+
async register({ name, email, password }) {
|
|
91
|
+
const exists = await prisma.user.findUnique({ where: { email } });
|
|
92
|
+
if (exists) throw new AppError('Email already registered', 409);
|
|
93
|
+
|
|
94
|
+
const passwordHash = await bcrypt.hash(password, 12);
|
|
95
|
+
|
|
96
|
+
const user = await prisma.user.create({
|
|
97
|
+
data: { name, email, password: passwordHash },
|
|
98
|
+
select: { id: true, name: true, email: true, role: true, createdAt: true },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const token = signToken(user.id);
|
|
102
|
+
return { user, token };
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async login({ email, password }) {
|
|
106
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
107
|
+
if (!user) throw new AppError('Invalid credentials', 401);
|
|
108
|
+
|
|
109
|
+
const valid = await bcrypt.compare(password, user.password);
|
|
110
|
+
if (!valid) throw new AppError('Invalid credentials', 401);
|
|
111
|
+
|
|
112
|
+
const { password: _, ...safeUser } = user;
|
|
113
|
+
const token = signToken(safeUser.id);
|
|
114
|
+
return { user: safeUser, token };
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async me(userId) {
|
|
118
|
+
const user = await prisma.user.findUnique({
|
|
119
|
+
where: { id: userId },
|
|
120
|
+
select: { id: true, name: true, email: true, role: true, createdAt: true },
|
|
121
|
+
});
|
|
122
|
+
if (!user) throw new AppError('User not found', 404);
|
|
123
|
+
return user;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### auth.controller.js
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { authService } from './auth.service.js';
|
|
132
|
+
import { sendSuccess } from '../../utils/response.js';
|
|
133
|
+
|
|
134
|
+
export const authController = {
|
|
135
|
+
register: async (req, res) => {
|
|
136
|
+
const result = await authService.register(req.body);
|
|
137
|
+
sendSuccess(res, 'Registered successfully', result, 201);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
login: async (req, res) => {
|
|
141
|
+
const result = await authService.login(req.body);
|
|
142
|
+
sendSuccess(res, 'Login successful', result);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
me: async (req, res) => {
|
|
146
|
+
const user = await authService.me(req.user.id);
|
|
147
|
+
sendSuccess(res, 'Profile fetched', user);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### auth.routes.js
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
import { Router } from 'express';
|
|
156
|
+
import { validate } from '../../middlewares/validate.middleware.js';
|
|
157
|
+
import { authenticate } from '../../middlewares/auth.middleware.js';
|
|
158
|
+
import { registerSchema, loginSchema } from './auth.validation.js';
|
|
159
|
+
import { authController } from './auth.controller.js';
|
|
160
|
+
|
|
161
|
+
const router = Router();
|
|
162
|
+
|
|
163
|
+
router.post('/register', validate(registerSchema), authController.register);
|
|
164
|
+
router.post('/login', validate(loginSchema), authController.login);
|
|
165
|
+
router.get('/me', authenticate, authController.me);
|
|
166
|
+
|
|
167
|
+
export default router;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Prisma User Model (minimum)
|
|
173
|
+
|
|
174
|
+
```prisma
|
|
175
|
+
model User {
|
|
176
|
+
id String @id @default(uuid())
|
|
177
|
+
name String
|
|
178
|
+
email String @unique
|
|
179
|
+
password String
|
|
180
|
+
role Role @default(USER)
|
|
181
|
+
status Status @default(ACTIVE)
|
|
182
|
+
createdAt DateTime @default(now())
|
|
183
|
+
updatedAt DateTime @updatedAt
|
|
184
|
+
|
|
185
|
+
@@map("users")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
enum Role { USER ADMIN MANAGER }
|
|
189
|
+
enum Status { ACTIVE SUSPENDED DELETED }
|
|
190
|
+
```
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Boilerplate Reference
|
|
2
|
+
|
|
3
|
+
Express v5 project setup — app.js, server.js, env validation, Prisma singleton.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## package.json
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"type": "module",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "nodemon src/server.js",
|
|
14
|
+
"start": "node src/server.js",
|
|
15
|
+
"migrate": "prisma migrate dev",
|
|
16
|
+
"studio": "prisma studio",
|
|
17
|
+
"seed": "node prisma/seed.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@prisma/client": "^5.0.0",
|
|
21
|
+
"bcrypt": "^6.0.0",
|
|
22
|
+
"cors": "^2.8.5",
|
|
23
|
+
"dotenv": "^16.0.0",
|
|
24
|
+
"express": "^5.0.0",
|
|
25
|
+
"express-rate-limit":"^7.0.0",
|
|
26
|
+
"helmet": "^8.0.0",
|
|
27
|
+
"jsonwebtoken": "^9.0.0",
|
|
28
|
+
"morgan": "^1.10.0",
|
|
29
|
+
"zod": "^4.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"nodemon": "^3.0.0",
|
|
33
|
+
"prisma": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## src/config/env.js
|
|
41
|
+
|
|
42
|
+
Crash at startup if any required variable is missing.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { z } from 'zod';
|
|
46
|
+
import 'dotenv/config';
|
|
47
|
+
|
|
48
|
+
const schema = z.object({
|
|
49
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
50
|
+
PORT: z.coerce.number().default(3000),
|
|
51
|
+
DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
|
|
52
|
+
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be ≥32 chars'),
|
|
53
|
+
JWT_EXPIRES_IN: z.string().default('7d'),
|
|
54
|
+
CORS_ORIGIN: z.string().default('*'),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const result = schema.safeParse(process.env);
|
|
58
|
+
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
console.error('❌ Invalid environment variables:');
|
|
61
|
+
result.error.issues.forEach((i) => console.error(` • ${i.path.join('.')}: ${i.message}`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const env = result.data;
|
|
66
|
+
export const isDev = env.NODE_ENV === 'development';
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## src/config/db.js — Prisma singleton
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import { PrismaClient } from '@prisma/client';
|
|
75
|
+
import { isDev } from './env.js';
|
|
76
|
+
|
|
77
|
+
const globalForPrisma = globalThis;
|
|
78
|
+
|
|
79
|
+
export const prisma =
|
|
80
|
+
globalForPrisma.prisma ??
|
|
81
|
+
new PrismaClient({ log: isDev ? ['query', 'warn', 'error'] : ['error'] });
|
|
82
|
+
|
|
83
|
+
if (!isDev) globalForPrisma.prisma = prisma;
|
|
84
|
+
|
|
85
|
+
export default prisma;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## src/app.js
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import express from 'express';
|
|
94
|
+
import helmet from 'helmet';
|
|
95
|
+
import cors from 'cors';
|
|
96
|
+
import morgan from 'morgan';
|
|
97
|
+
import rateLimit from 'express-rate-limit';
|
|
98
|
+
import { router } from './routes/index.js';
|
|
99
|
+
import { errorHandler, notFound } from './middlewares/errorHandler.js';
|
|
100
|
+
import { env, isDev } from './config/env.js';
|
|
101
|
+
|
|
102
|
+
const app = express();
|
|
103
|
+
|
|
104
|
+
// Security
|
|
105
|
+
app.use(helmet());
|
|
106
|
+
app.use(cors({ origin: env.CORS_ORIGIN, credentials: true }));
|
|
107
|
+
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true }));
|
|
108
|
+
|
|
109
|
+
// Parsing
|
|
110
|
+
app.use(express.json({ limit: '10mb' }));
|
|
111
|
+
app.use(express.urlencoded({ extended: true }));
|
|
112
|
+
|
|
113
|
+
// Logging
|
|
114
|
+
if (isDev) app.use(morgan('dev'));
|
|
115
|
+
|
|
116
|
+
// Health check (no auth required)
|
|
117
|
+
app.get('/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
|
118
|
+
|
|
119
|
+
// API routes
|
|
120
|
+
app.use('/api/v1', router);
|
|
121
|
+
|
|
122
|
+
// Error handling — must be last
|
|
123
|
+
app.use(notFound);
|
|
124
|
+
app.use(errorHandler);
|
|
125
|
+
|
|
126
|
+
export { app };
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## server.js
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import { app } from './src/app.js';
|
|
135
|
+
import { prisma } from './src/config/db.js';
|
|
136
|
+
import { env } from './src/config/env.js';
|
|
137
|
+
|
|
138
|
+
const start = async () => {
|
|
139
|
+
try {
|
|
140
|
+
await prisma.$connect();
|
|
141
|
+
console.log('✅ Database connected');
|
|
142
|
+
|
|
143
|
+
const server = app.listen(env.PORT, () => {
|
|
144
|
+
console.log(`🚀 Server running on port ${env.PORT} [${env.NODE_ENV}]`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Graceful shutdown
|
|
148
|
+
const shutdown = async (signal) => {
|
|
149
|
+
console.log(`\n${signal} received — shutting down gracefully`);
|
|
150
|
+
server.close(async () => {
|
|
151
|
+
await prisma.$disconnect();
|
|
152
|
+
console.log('Database disconnected. Bye 👋');
|
|
153
|
+
process.exit(0);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
158
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
159
|
+
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('❌ Startup failed:', err);
|
|
162
|
+
await prisma.$disconnect();
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
start();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## src/routes/index.js
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { Router } from 'express';
|
|
176
|
+
// import userRouter from '../modules/user/user.routes.js';
|
|
177
|
+
// import authRouter from '../modules/auth/auth.routes.js';
|
|
178
|
+
|
|
179
|
+
export const router = Router();
|
|
180
|
+
|
|
181
|
+
// router.use('/auth', authRouter);
|
|
182
|
+
// router.use('/users', userRouter);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## .env.example
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
NODE_ENV=development
|
|
191
|
+
PORT=3000
|
|
192
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
193
|
+
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
|
|
194
|
+
JWT_EXPIRES_IN=7d
|
|
195
|
+
CORS_ORIGIN=http://localhost:5173
|
|
196
|
+
```
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Error Handling Reference
|
|
2
|
+
|
|
3
|
+
Express v5 + AppError pattern. Errors thrown anywhere in async route handlers automatically reach `errorHandler`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## src/utils/AppError.js
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
export class AppError extends Error {
|
|
11
|
+
constructor(message, statusCode = 500, errors = null) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.status = statusCode >= 400 && statusCode < 500 ? 'fail' : 'error';
|
|
15
|
+
this.isOperational = true;
|
|
16
|
+
this.errors = errors; // optional: array of { field, message }
|
|
17
|
+
Error.captureStackTrace(this, this.constructor);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## src/middlewares/errorHandler.js
|
|
25
|
+
|
|
26
|
+
Handles all error types in one place. Mount last in `app.js`.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { AppError } from '../utils/AppError.js';
|
|
30
|
+
import { Prisma } from '@prisma/client';
|
|
31
|
+
|
|
32
|
+
export const errorHandler = (err, req, res, next) => {
|
|
33
|
+
let statusCode = err.statusCode || 500;
|
|
34
|
+
let message = err.message || 'Internal server error';
|
|
35
|
+
let errors = err.errors || null;
|
|
36
|
+
|
|
37
|
+
// Prisma known errors
|
|
38
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
39
|
+
switch (err.code) {
|
|
40
|
+
case 'P2002': statusCode = 409; message = `Duplicate: ${err.meta?.target}`; break;
|
|
41
|
+
case 'P2025': statusCode = 404; message = 'Record not found'; break;
|
|
42
|
+
case 'P2003': statusCode = 400; message = 'Related record not found'; break;
|
|
43
|
+
case 'P2014': statusCode = 400; message = 'Relation constraint failed'; break;
|
|
44
|
+
default: statusCode = 500; message = 'Database error';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// JWT errors
|
|
49
|
+
if (err.name === 'JsonWebTokenError') { statusCode = 401; message = 'Invalid token'; }
|
|
50
|
+
if (err.name === 'TokenExpiredError') { statusCode = 401; message = 'Token expired. Please log in again'; }
|
|
51
|
+
|
|
52
|
+
// Zod error (if validation middleware was bypassed)
|
|
53
|
+
if (err.name === 'ZodError') {
|
|
54
|
+
statusCode = 422;
|
|
55
|
+
message = 'Validation failed';
|
|
56
|
+
errors = err.issues.map((i) => ({ field: i.path.join('.'), message: i.message }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
60
|
+
|
|
61
|
+
res.status(statusCode).json({
|
|
62
|
+
success: false,
|
|
63
|
+
message,
|
|
64
|
+
...(errors && { errors }),
|
|
65
|
+
...(isDev && !err.isOperational && { stack: err.stack }),
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const notFound = (req, res, next) => {
|
|
70
|
+
next(new AppError(`Route not found: ${req.method} ${req.originalUrl}`, 404));
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## src/app.js — Error middleware order
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
// Routes
|
|
80
|
+
app.use('/api/v1', router);
|
|
81
|
+
|
|
82
|
+
// Must come AFTER routes
|
|
83
|
+
app.use(notFound);
|
|
84
|
+
app.use(errorHandler);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Express v5 — Async Error Propagation
|
|
90
|
+
|
|
91
|
+
Express v5 automatically catches rejected promises from async route handlers.
|
|
92
|
+
**No `catchAsync` / `asyncHandler` wrapper needed.**
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// Service layer — throw for business errors
|
|
96
|
+
export const getById = async (id) => {
|
|
97
|
+
const item = await prisma.product.findUnique({ where: { id } });
|
|
98
|
+
if (!item) throw new AppError('Product not found', 404); // ← propagates automatically
|
|
99
|
+
return item;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Controller — no try/catch
|
|
103
|
+
export const getById = async (req, res) => {
|
|
104
|
+
const item = await productService.getById(req.params.id); // throws → errorHandler
|
|
105
|
+
sendSuccess(res, 'Product fetched', item);
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Where Each Error Is Thrown
|
|
112
|
+
|
|
113
|
+
| Location | Pattern |
|
|
114
|
+
|----------|---------|
|
|
115
|
+
| Service — resource not found | `throw new AppError('X not found', 404)` |
|
|
116
|
+
| Service — conflict | `throw new AppError('Already exists', 409)` |
|
|
117
|
+
| Service — business rule | `throw new AppError('Insufficient balance', 400)` |
|
|
118
|
+
| Service — forbidden | `throw new AppError('Cannot perform this action', 403)` |
|
|
119
|
+
| Repository / service — Prisma | catch `PrismaClientKnownRequestError` → `throw new AppError(...)` |
|
|
120
|
+
| Middleware — auth | `return next(new AppError('No token', 401))` |
|
|
121
|
+
| Controller | **Never throw directly** — delegate to service |
|