@arcadialdev/arcality 2.2.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/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
- package/.agents/skills/frontend-design/LICENSE.txt +177 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
- package/.agents/skills/playwright-best-practices/README.md +147 -0
- package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
- package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
- package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
- package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
- package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
- package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
- package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
- package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
- package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
- package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
- package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
- package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
- package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
- package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
- package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
- package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
- package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
- package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
- package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
- package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
- package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
- package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
- package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
- package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
- package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
- package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
- package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
- package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
- package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
- package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
- package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
- package/.env.example +21 -0
- package/README.md +30 -0
- package/bin/arcality.mjs +86 -0
- package/package.json +66 -0
- package/playwright.config.ts +12 -0
- package/scripts/cleanup-qmsdev.mjs +63 -0
- package/scripts/discover-view.mjs +52 -0
- package/scripts/extract-view.mjs +64 -0
- package/scripts/gen-and-run.mjs +838 -0
- package/scripts/init.mjs +290 -0
- package/scripts/migrate-to-central-out.mjs +157 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/rebrand-report.mjs +241 -0
- package/scripts/setup.mjs +166 -0
- package/src/KnowledgeService.ts +239 -0
- package/src/arcalityClient.mjs +266 -0
- package/src/configLoader.mjs +179 -0
- package/src/configManager.mjs +172 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +490 -0
- package/tests/_helpers/agentic-runner.spec.ts +741 -0
- package/tests/_helpers/ai-agent-helper.ts +1573 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodejs-backend-patterns
|
|
3
|
+
description: Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Node.js Backend Patterns
|
|
7
|
+
|
|
8
|
+
Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Building REST APIs or GraphQL servers
|
|
13
|
+
- Creating microservices with Node.js
|
|
14
|
+
- Implementing authentication and authorization
|
|
15
|
+
- Designing scalable backend architectures
|
|
16
|
+
- Setting up middleware and error handling
|
|
17
|
+
- Integrating databases (SQL and NoSQL)
|
|
18
|
+
- Building real-time applications with WebSockets
|
|
19
|
+
- Implementing background job processing
|
|
20
|
+
|
|
21
|
+
## Core Frameworks
|
|
22
|
+
|
|
23
|
+
### Express.js - Minimalist Framework
|
|
24
|
+
|
|
25
|
+
**Basic Setup:**
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import express, { Request, Response, NextFunction } from "express";
|
|
29
|
+
import helmet from "helmet";
|
|
30
|
+
import cors from "cors";
|
|
31
|
+
import compression from "compression";
|
|
32
|
+
|
|
33
|
+
const app = express();
|
|
34
|
+
|
|
35
|
+
// Security middleware
|
|
36
|
+
app.use(helmet());
|
|
37
|
+
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
|
|
38
|
+
app.use(compression());
|
|
39
|
+
|
|
40
|
+
// Body parsing
|
|
41
|
+
app.use(express.json({ limit: "10mb" }));
|
|
42
|
+
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
43
|
+
|
|
44
|
+
// Request logging
|
|
45
|
+
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
46
|
+
console.log(`${req.method} ${req.path}`);
|
|
47
|
+
next();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const PORT = process.env.PORT || 3000;
|
|
51
|
+
app.listen(PORT, () => {
|
|
52
|
+
console.log(`Server running on port ${PORT}`);
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Fastify - High Performance Framework
|
|
57
|
+
|
|
58
|
+
**Basic Setup:**
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import Fastify from "fastify";
|
|
62
|
+
import helmet from "@fastify/helmet";
|
|
63
|
+
import cors from "@fastify/cors";
|
|
64
|
+
import compress from "@fastify/compress";
|
|
65
|
+
|
|
66
|
+
const fastify = Fastify({
|
|
67
|
+
logger: {
|
|
68
|
+
level: process.env.LOG_LEVEL || "info",
|
|
69
|
+
transport: {
|
|
70
|
+
target: "pino-pretty",
|
|
71
|
+
options: { colorize: true },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Plugins
|
|
77
|
+
await fastify.register(helmet);
|
|
78
|
+
await fastify.register(cors, { origin: true });
|
|
79
|
+
await fastify.register(compress);
|
|
80
|
+
|
|
81
|
+
// Type-safe routes with schema validation
|
|
82
|
+
fastify.post<{
|
|
83
|
+
Body: { name: string; email: string };
|
|
84
|
+
Reply: { id: string; name: string };
|
|
85
|
+
}>(
|
|
86
|
+
"/users",
|
|
87
|
+
{
|
|
88
|
+
schema: {
|
|
89
|
+
body: {
|
|
90
|
+
type: "object",
|
|
91
|
+
required: ["name", "email"],
|
|
92
|
+
properties: {
|
|
93
|
+
name: { type: "string", minLength: 1 },
|
|
94
|
+
email: { type: "string", format: "email" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
async (request, reply) => {
|
|
100
|
+
const { name, email } = request.body;
|
|
101
|
+
return { id: "123", name };
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await fastify.listen({ port: 3000, host: "0.0.0.0" });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Architectural Patterns
|
|
109
|
+
|
|
110
|
+
### Pattern 1: Layered Architecture
|
|
111
|
+
|
|
112
|
+
**Structure:**
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
src/
|
|
116
|
+
├── controllers/ # Handle HTTP requests/responses
|
|
117
|
+
├── services/ # Business logic
|
|
118
|
+
├── repositories/ # Data access layer
|
|
119
|
+
├── models/ # Data models
|
|
120
|
+
├── middleware/ # Express/Fastify middleware
|
|
121
|
+
├── routes/ # Route definitions
|
|
122
|
+
├── utils/ # Helper functions
|
|
123
|
+
├── config/ # Configuration
|
|
124
|
+
└── types/ # TypeScript types
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Controller Layer:**
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// controllers/user.controller.ts
|
|
131
|
+
import { Request, Response, NextFunction } from "express";
|
|
132
|
+
import { UserService } from "../services/user.service";
|
|
133
|
+
import { CreateUserDTO, UpdateUserDTO } from "../types/user.types";
|
|
134
|
+
|
|
135
|
+
export class UserController {
|
|
136
|
+
constructor(private userService: UserService) {}
|
|
137
|
+
|
|
138
|
+
async createUser(req: Request, res: Response, next: NextFunction) {
|
|
139
|
+
try {
|
|
140
|
+
const userData: CreateUserDTO = req.body;
|
|
141
|
+
const user = await this.userService.createUser(userData);
|
|
142
|
+
res.status(201).json(user);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
next(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getUser(req: Request, res: Response, next: NextFunction) {
|
|
149
|
+
try {
|
|
150
|
+
const { id } = req.params;
|
|
151
|
+
const user = await this.userService.getUserById(id);
|
|
152
|
+
res.json(user);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
next(error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async updateUser(req: Request, res: Response, next: NextFunction) {
|
|
159
|
+
try {
|
|
160
|
+
const { id } = req.params;
|
|
161
|
+
const updates: UpdateUserDTO = req.body;
|
|
162
|
+
const user = await this.userService.updateUser(id, updates);
|
|
163
|
+
res.json(user);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
next(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteUser(req: Request, res: Response, next: NextFunction) {
|
|
170
|
+
try {
|
|
171
|
+
const { id } = req.params;
|
|
172
|
+
await this.userService.deleteUser(id);
|
|
173
|
+
res.status(204).send();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
next(error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Service Layer:**
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// services/user.service.ts
|
|
185
|
+
import { UserRepository } from "../repositories/user.repository";
|
|
186
|
+
import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types";
|
|
187
|
+
import { NotFoundError, ValidationError } from "../utils/errors";
|
|
188
|
+
import bcrypt from "bcrypt";
|
|
189
|
+
|
|
190
|
+
export class UserService {
|
|
191
|
+
constructor(private userRepository: UserRepository) {}
|
|
192
|
+
|
|
193
|
+
async createUser(userData: CreateUserDTO): Promise<User> {
|
|
194
|
+
// Validation
|
|
195
|
+
const existingUser = await this.userRepository.findByEmail(userData.email);
|
|
196
|
+
if (existingUser) {
|
|
197
|
+
throw new ValidationError("Email already exists");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Hash password
|
|
201
|
+
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
|
202
|
+
|
|
203
|
+
// Create user
|
|
204
|
+
const user = await this.userRepository.create({
|
|
205
|
+
...userData,
|
|
206
|
+
password: hashedPassword,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Remove password from response
|
|
210
|
+
const { password, ...userWithoutPassword } = user;
|
|
211
|
+
return userWithoutPassword as User;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getUserById(id: string): Promise<User> {
|
|
215
|
+
const user = await this.userRepository.findById(id);
|
|
216
|
+
if (!user) {
|
|
217
|
+
throw new NotFoundError("User not found");
|
|
218
|
+
}
|
|
219
|
+
const { password, ...userWithoutPassword } = user;
|
|
220
|
+
return userWithoutPassword as User;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
|
|
224
|
+
const user = await this.userRepository.update(id, updates);
|
|
225
|
+
if (!user) {
|
|
226
|
+
throw new NotFoundError("User not found");
|
|
227
|
+
}
|
|
228
|
+
const { password, ...userWithoutPassword } = user;
|
|
229
|
+
return userWithoutPassword as User;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async deleteUser(id: string): Promise<void> {
|
|
233
|
+
const deleted = await this.userRepository.delete(id);
|
|
234
|
+
if (!deleted) {
|
|
235
|
+
throw new NotFoundError("User not found");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Repository Layer:**
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// repositories/user.repository.ts
|
|
245
|
+
import { Pool } from "pg";
|
|
246
|
+
import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types";
|
|
247
|
+
|
|
248
|
+
export class UserRepository {
|
|
249
|
+
constructor(private db: Pool) {}
|
|
250
|
+
|
|
251
|
+
async create(
|
|
252
|
+
userData: CreateUserDTO & { password: string },
|
|
253
|
+
): Promise<UserEntity> {
|
|
254
|
+
const query = `
|
|
255
|
+
INSERT INTO users (name, email, password)
|
|
256
|
+
VALUES ($1, $2, $3)
|
|
257
|
+
RETURNING id, name, email, password, created_at, updated_at
|
|
258
|
+
`;
|
|
259
|
+
const { rows } = await this.db.query(query, [
|
|
260
|
+
userData.name,
|
|
261
|
+
userData.email,
|
|
262
|
+
userData.password,
|
|
263
|
+
]);
|
|
264
|
+
return rows[0];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async findById(id: string): Promise<UserEntity | null> {
|
|
268
|
+
const query = "SELECT * FROM users WHERE id = $1";
|
|
269
|
+
const { rows } = await this.db.query(query, [id]);
|
|
270
|
+
return rows[0] || null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async findByEmail(email: string): Promise<UserEntity | null> {
|
|
274
|
+
const query = "SELECT * FROM users WHERE email = $1";
|
|
275
|
+
const { rows } = await this.db.query(query, [email]);
|
|
276
|
+
return rows[0] || null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {
|
|
280
|
+
const fields = Object.keys(updates);
|
|
281
|
+
const values = Object.values(updates);
|
|
282
|
+
|
|
283
|
+
const setClause = fields
|
|
284
|
+
.map((field, idx) => `${field} = $${idx + 2}`)
|
|
285
|
+
.join(", ");
|
|
286
|
+
|
|
287
|
+
const query = `
|
|
288
|
+
UPDATE users
|
|
289
|
+
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
|
|
290
|
+
WHERE id = $1
|
|
291
|
+
RETURNING *
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
const { rows } = await this.db.query(query, [id, ...values]);
|
|
295
|
+
return rows[0] || null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async delete(id: string): Promise<boolean> {
|
|
299
|
+
const query = "DELETE FROM users WHERE id = $1";
|
|
300
|
+
const { rowCount } = await this.db.query(query, [id]);
|
|
301
|
+
return rowCount > 0;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Pattern 2: Dependency Injection
|
|
307
|
+
|
|
308
|
+
Use a DI container to wire up repositories, services, and controllers. For a full container implementation, see [references/advanced-patterns.md](references/advanced-patterns.md).
|
|
309
|
+
|
|
310
|
+
## Middleware Patterns
|
|
311
|
+
|
|
312
|
+
### Authentication Middleware
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// middleware/auth.middleware.ts
|
|
316
|
+
import { Request, Response, NextFunction } from "express";
|
|
317
|
+
import jwt from "jsonwebtoken";
|
|
318
|
+
import { UnauthorizedError } from "../utils/errors";
|
|
319
|
+
|
|
320
|
+
interface JWTPayload {
|
|
321
|
+
userId: string;
|
|
322
|
+
email: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
declare global {
|
|
326
|
+
namespace Express {
|
|
327
|
+
interface Request {
|
|
328
|
+
user?: JWTPayload;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export const authenticate = async (
|
|
334
|
+
req: Request,
|
|
335
|
+
res: Response,
|
|
336
|
+
next: NextFunction,
|
|
337
|
+
) => {
|
|
338
|
+
try {
|
|
339
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
340
|
+
|
|
341
|
+
if (!token) {
|
|
342
|
+
throw new UnauthorizedError("No token provided");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
|
346
|
+
|
|
347
|
+
req.user = payload;
|
|
348
|
+
next();
|
|
349
|
+
} catch (error) {
|
|
350
|
+
next(new UnauthorizedError("Invalid token"));
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const authorize = (...roles: string[]) => {
|
|
355
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
356
|
+
if (!req.user) {
|
|
357
|
+
return next(new UnauthorizedError("Not authenticated"));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check if user has required role
|
|
361
|
+
const hasRole = roles.some((role) => req.user?.roles?.includes(role));
|
|
362
|
+
|
|
363
|
+
if (!hasRole) {
|
|
364
|
+
return next(new UnauthorizedError("Insufficient permissions"));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
next();
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Validation Middleware
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// middleware/validation.middleware.ts
|
|
376
|
+
import { Request, Response, NextFunction } from "express";
|
|
377
|
+
import { AnyZodObject, ZodError } from "zod";
|
|
378
|
+
import { ValidationError } from "../utils/errors";
|
|
379
|
+
|
|
380
|
+
export const validate = (schema: AnyZodObject) => {
|
|
381
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
382
|
+
try {
|
|
383
|
+
await schema.parseAsync({
|
|
384
|
+
body: req.body,
|
|
385
|
+
query: req.query,
|
|
386
|
+
params: req.params,
|
|
387
|
+
});
|
|
388
|
+
next();
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error instanceof ZodError) {
|
|
391
|
+
const errors = error.errors.map((err) => ({
|
|
392
|
+
field: err.path.join("."),
|
|
393
|
+
message: err.message,
|
|
394
|
+
}));
|
|
395
|
+
next(new ValidationError("Validation failed", errors));
|
|
396
|
+
} else {
|
|
397
|
+
next(error);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Usage with Zod
|
|
404
|
+
import { z } from "zod";
|
|
405
|
+
|
|
406
|
+
const createUserSchema = z.object({
|
|
407
|
+
body: z.object({
|
|
408
|
+
name: z.string().min(1),
|
|
409
|
+
email: z.string().email(),
|
|
410
|
+
password: z.string().min(8),
|
|
411
|
+
}),
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
router.post("/users", validate(createUserSchema), userController.createUser);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Rate Limiting Middleware
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// middleware/rate-limit.middleware.ts
|
|
421
|
+
import rateLimit from "express-rate-limit";
|
|
422
|
+
import RedisStore from "rate-limit-redis";
|
|
423
|
+
import Redis from "ioredis";
|
|
424
|
+
|
|
425
|
+
const redis = new Redis({
|
|
426
|
+
host: process.env.REDIS_HOST,
|
|
427
|
+
port: parseInt(process.env.REDIS_PORT || "6379"),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
export const apiLimiter = rateLimit({
|
|
431
|
+
store: new RedisStore({
|
|
432
|
+
client: redis,
|
|
433
|
+
prefix: "rl:",
|
|
434
|
+
}),
|
|
435
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
436
|
+
max: 100, // Limit each IP to 100 requests per windowMs
|
|
437
|
+
message: "Too many requests from this IP, please try again later",
|
|
438
|
+
standardHeaders: true,
|
|
439
|
+
legacyHeaders: false,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
export const authLimiter = rateLimit({
|
|
443
|
+
store: new RedisStore({
|
|
444
|
+
client: redis,
|
|
445
|
+
prefix: "rl:auth:",
|
|
446
|
+
}),
|
|
447
|
+
windowMs: 15 * 60 * 1000,
|
|
448
|
+
max: 5, // Stricter limit for auth endpoints
|
|
449
|
+
skipSuccessfulRequests: true,
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Request Logging Middleware
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// middleware/logger.middleware.ts
|
|
457
|
+
import { Request, Response, NextFunction } from "express";
|
|
458
|
+
import pino from "pino";
|
|
459
|
+
|
|
460
|
+
const logger = pino({
|
|
461
|
+
level: process.env.LOG_LEVEL || "info",
|
|
462
|
+
transport: {
|
|
463
|
+
target: "pino-pretty",
|
|
464
|
+
options: { colorize: true },
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
export const requestLogger = (
|
|
469
|
+
req: Request,
|
|
470
|
+
res: Response,
|
|
471
|
+
next: NextFunction,
|
|
472
|
+
) => {
|
|
473
|
+
const start = Date.now();
|
|
474
|
+
|
|
475
|
+
// Log response when finished
|
|
476
|
+
res.on("finish", () => {
|
|
477
|
+
const duration = Date.now() - start;
|
|
478
|
+
logger.info({
|
|
479
|
+
method: req.method,
|
|
480
|
+
url: req.url,
|
|
481
|
+
status: res.statusCode,
|
|
482
|
+
duration: `${duration}ms`,
|
|
483
|
+
userAgent: req.headers["user-agent"],
|
|
484
|
+
ip: req.ip,
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
next();
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export { logger };
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Error Handling
|
|
495
|
+
|
|
496
|
+
### Custom Error Classes
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
// utils/errors.ts
|
|
500
|
+
export class AppError extends Error {
|
|
501
|
+
constructor(
|
|
502
|
+
public message: string,
|
|
503
|
+
public statusCode: number = 500,
|
|
504
|
+
public isOperational: boolean = true,
|
|
505
|
+
) {
|
|
506
|
+
super(message);
|
|
507
|
+
Object.setPrototypeOf(this, AppError.prototype);
|
|
508
|
+
Error.captureStackTrace(this, this.constructor);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export class ValidationError extends AppError {
|
|
513
|
+
constructor(
|
|
514
|
+
message: string,
|
|
515
|
+
public errors?: any[],
|
|
516
|
+
) {
|
|
517
|
+
super(message, 400);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export class NotFoundError extends AppError {
|
|
522
|
+
constructor(message: string = "Resource not found") {
|
|
523
|
+
super(message, 404);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export class UnauthorizedError extends AppError {
|
|
528
|
+
constructor(message: string = "Unauthorized") {
|
|
529
|
+
super(message, 401);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export class ForbiddenError extends AppError {
|
|
534
|
+
constructor(message: string = "Forbidden") {
|
|
535
|
+
super(message, 403);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export class ConflictError extends AppError {
|
|
540
|
+
constructor(message: string) {
|
|
541
|
+
super(message, 409);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Global Error Handler
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
// middleware/error-handler.ts
|
|
550
|
+
import { Request, Response, NextFunction } from "express";
|
|
551
|
+
import { AppError } from "../utils/errors";
|
|
552
|
+
import { logger } from "./logger.middleware";
|
|
553
|
+
|
|
554
|
+
export const errorHandler = (
|
|
555
|
+
err: Error,
|
|
556
|
+
req: Request,
|
|
557
|
+
res: Response,
|
|
558
|
+
next: NextFunction,
|
|
559
|
+
) => {
|
|
560
|
+
if (err instanceof AppError) {
|
|
561
|
+
return res.status(err.statusCode).json({
|
|
562
|
+
status: "error",
|
|
563
|
+
message: err.message,
|
|
564
|
+
...(err instanceof ValidationError && { errors: err.errors }),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Log unexpected errors
|
|
569
|
+
logger.error({
|
|
570
|
+
error: err.message,
|
|
571
|
+
stack: err.stack,
|
|
572
|
+
url: req.url,
|
|
573
|
+
method: req.method,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Don't leak error details in production
|
|
577
|
+
const message =
|
|
578
|
+
process.env.NODE_ENV === "production"
|
|
579
|
+
? "Internal server error"
|
|
580
|
+
: err.message;
|
|
581
|
+
|
|
582
|
+
res.status(500).json({
|
|
583
|
+
status: "error",
|
|
584
|
+
message,
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// Async error wrapper
|
|
589
|
+
export const asyncHandler = (
|
|
590
|
+
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
|
|
591
|
+
) => {
|
|
592
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
593
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
594
|
+
};
|
|
595
|
+
};
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
## Database Patterns
|
|
599
|
+
|
|
600
|
+
Node.js supports both SQL and NoSQL databases. Use connection pooling for all production databases.
|
|
601
|
+
|
|
602
|
+
Key patterns covered in [references/advanced-patterns.md](references/advanced-patterns.md):
|
|
603
|
+
- **PostgreSQL with connection pool** — `pg` Pool configuration and graceful shutdown
|
|
604
|
+
- **MongoDB with Mongoose** — connection management and schema definition
|
|
605
|
+
- **Transaction pattern** — `BEGIN`/`COMMIT`/`ROLLBACK` with `pg` client
|
|
606
|
+
|
|
607
|
+
## Authentication & Authorization
|
|
608
|
+
|
|
609
|
+
JWT-based auth with access tokens (short-lived, 15m) and refresh tokens (7d). Full `AuthService` implementation with `bcrypt` password comparison in [references/advanced-patterns.md](references/advanced-patterns.md).
|
|
610
|
+
|
|
611
|
+
## Caching Strategies
|
|
612
|
+
|
|
613
|
+
Redis-backed `CacheService` with get/set/delete/invalidatePattern, plus a `@Cacheable` decorator for method-level caching. See [references/advanced-patterns.md](references/advanced-patterns.md).
|
|
614
|
+
|
|
615
|
+
## API Response Format
|
|
616
|
+
|
|
617
|
+
Standardized `ApiResponse` helper with `success`, `error`, and `paginated` static methods. See [references/advanced-patterns.md](references/advanced-patterns.md).
|
|
618
|
+
|
|
619
|
+
## Best Practices
|
|
620
|
+
|
|
621
|
+
1. **Use TypeScript**: Type safety prevents runtime errors
|
|
622
|
+
2. **Implement proper error handling**: Use custom error classes
|
|
623
|
+
3. **Validate input**: Use libraries like Zod or Joi
|
|
624
|
+
4. **Use environment variables**: Never hardcode secrets
|
|
625
|
+
5. **Implement logging**: Use structured logging (Pino, Winston)
|
|
626
|
+
6. **Add rate limiting**: Prevent abuse
|
|
627
|
+
7. **Use HTTPS**: Always in production
|
|
628
|
+
8. **Implement CORS properly**: Don't use `*` in production
|
|
629
|
+
9. **Use dependency injection**: Easier testing and maintenance
|
|
630
|
+
10. **Write tests**: Unit, integration, and E2E tests
|
|
631
|
+
11. **Handle graceful shutdown**: Clean up resources
|
|
632
|
+
12. **Use connection pooling**: For databases
|
|
633
|
+
13. **Implement health checks**: For monitoring
|
|
634
|
+
14. **Use compression**: Reduce response size
|
|
635
|
+
15. **Monitor performance**: Use APM tools
|
|
636
|
+
|
|
637
|
+
## Testing Patterns
|
|
638
|
+
|
|
639
|
+
See `javascript-testing-patterns` skill for comprehensive testing guidance.
|