Files
claudekit/skills/backend-frameworks/references/express.md
T
2026-04-19 14:10:38 +07:00

452 lines
12 KiB
Markdown

# Backend Frameworks — Express Patterns
# Express
## Overview
Production patterns for building Node.js HTTP servers and REST APIs with Express. Covers routing, middleware, validation, error handling, authentication, database integration, and testing.
## When to Use
- Building REST APIs with Express (without NestJS)
- Adding middleware (auth, logging, rate limiting, CORS)
- Handling file uploads, streaming, or WebSockets on Express
- Migrating Express apps or adding features to existing ones
## When NOT to Use
- **NestJS projects** — use the `nestjs` skill (NestJS wraps Express but has its own patterns)
- **FastAPI / Django** — use the `fastapi` or `django` skill
- **Frontend** — use `react` or `nextjs`
- **Cloudflare Workers / edge** — use `cloudflare-workers`
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| Project structure | SS Architecture below |
| Route patterns | SS Routing below |
| Middleware | SS Middleware below |
| Input validation | SS Validation below |
| Error handling | SS Error Handling below |
| Auth patterns | SS Authentication below |
| Database integration | SS Database below |
| Testing | SS Testing below |
---
## Architecture
### Project structure
```
src/
├── app.ts # Express app setup (middleware, routes)
├── server.ts # HTTP server bootstrap
├── routes/
│ ├── index.ts # Route aggregator
│ ├── users.routes.ts # /api/users
│ └── orders.routes.ts # /api/orders
├── middleware/
│ ├── auth.ts # JWT verification
│ ├── validate.ts # Zod validation middleware
│ ├── error-handler.ts # Global error handler
│ └── rate-limit.ts # Rate limiting
├── services/
│ ├── users.service.ts # Business logic
│ └── orders.service.ts
├── models/ # Prisma or TypeORM entities
├── utils/
│ └── async-handler.ts # Async error wrapper
└── tests/
├── users.test.ts
└── orders.test.ts
```
### App setup
```typescript
// src/app.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { json } from 'express';
import { router } from './routes';
import { errorHandler } from './middleware/error-handler';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
// Body parsing
app.use(json({ limit: '10kb' }));
// Routes
app.use('/api', router);
// Health check
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
// Global error handler (must be last)
app.use(errorHandler);
export { app };
```
```typescript
// src/server.ts
import { app } from './app';
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`Listening on :${PORT}`));
```
---
## Routing
### Router pattern
```typescript
// src/routes/users.routes.ts
import { Router } from 'express';
import { UsersService } from '../services/users.service';
import { validate } from '../middleware/validate';
import { createUserSchema, updateUserSchema } from '../schemas/user.schema';
import { asyncHandler } from '../utils/async-handler';
const router = Router();
const service = new UsersService();
router.post('/', validate(createUserSchema), asyncHandler(async (req, res) => {
const user = await service.create(req.body);
res.status(201).json(user);
}));
router.get('/:id', asyncHandler(async (req, res) => {
const user = await service.findOne(req.params.id);
if (!user) {
res.status(404).json({ type: 'not-found', title: 'Not Found', status: 404, detail: `User ${req.params.id} not found` });
return;
}
res.json(user);
}));
router.patch('/:id', validate(updateUserSchema), asyncHandler(async (req, res) => {
const user = await service.update(req.params.id, req.body);
res.json(user);
}));
router.delete('/:id', asyncHandler(async (req, res) => {
await service.remove(req.params.id);
res.status(204).end();
}));
export { router as usersRouter };
```
### Async error wrapper
```typescript
// src/utils/async-handler.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
export function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
): RequestHandler {
return (req, res, next) => fn(req, res, next).catch(next);
}
```
### Route aggregator
```typescript
// src/routes/index.ts
import { Router } from 'express';
import { usersRouter } from './users.routes';
import { ordersRouter } from './orders.routes';
const router = Router();
router.use('/users', usersRouter);
router.use('/orders', ordersRouter);
export { router };
```
---
## Middleware
### Middleware order matters
```typescript
// Correct order in app.ts:
app.use(helmet()); // 1. Security headers
app.use(cors()); // 2. CORS
app.use(json()); // 3. Body parsing
app.use(requestLogger); // 4. Logging
app.use(rateLimiter); // 5. Rate limiting
app.use('/api', router); // 6. Routes
app.use(errorHandler); // 7. Error handler (MUST be last)
```
### Request logging
```typescript
import { Request, Response, NextFunction } from 'express';
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
}
```
### Rate limiting
```typescript
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: { type: 'rate-limit', title: 'Too Many Requests', status: 429 },
});
```
---
## Validation
### Zod validation middleware
```typescript
// src/middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema, ZodError } from 'zod';
export function validate(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (err) {
if (err instanceof ZodError) {
res.status(400).json({
type: 'validation-error',
title: 'Bad Request',
status: 400,
detail: err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join('; '),
});
return;
}
next(err);
}
};
}
```
```typescript
// src/schemas/user.schema.ts
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.string().email().max(254),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'member', 'viewer']).default('member'),
});
export const updateUserSchema = createUserSchema.partial();
export type CreateUserInput = z.infer<typeof createUserSchema>;
```
---
## Error Handling
### Global error handler (RFC 9457 Problem Details)
```typescript
// src/middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction) {
const status = err instanceof AppError ? err.statusCode : 500;
const title = status >= 500 ? 'Internal Server Error' : err.message;
if (status >= 500) console.error(err);
res.status(status).json({
type: `https://api.example.com/problems/${status}`,
title,
status,
detail: status >= 500 ? 'An unexpected error occurred' : err.message,
});
}
```
---
## Authentication
### JWT middleware
```typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export interface AuthRequest extends Request {
user?: { sub: string; role: string };
}
export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
res.status(401).json({ type: 'unauthorized', title: 'Unauthorized', status: 401, detail: 'Missing bearer token' });
return;
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as AuthRequest['user'];
req.user = payload;
next();
} catch {
res.status(401).json({ type: 'unauthorized', title: 'Unauthorized', status: 401, detail: 'Invalid or expired token' });
}
}
export function authorize(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
res.status(403).json({ type: 'forbidden', title: 'Forbidden', status: 403, detail: 'Insufficient permissions' });
return;
}
next();
};
}
```
---
## Database
### Prisma integration
```typescript
// src/db.ts
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
// Graceful shutdown
process.on('SIGTERM', async () => {
await prisma.$disconnect();
process.exit(0);
});
```
```typescript
// src/services/users.service.ts
import { prisma } from '../db';
import { CreateUserInput } from '../schemas/user.schema';
export class UsersService {
async findOne(id: string) {
return prisma.user.findUnique({ where: { id } });
}
async create(data: CreateUserInput) {
return prisma.user.create({ data });
}
async update(id: string, data: Partial<CreateUserInput>) {
return prisma.user.update({ where: { id }, data });
}
async remove(id: string) {
await prisma.user.delete({ where: { id } });
}
}
```
---
## Testing
### Integration tests with supertest
```typescript
// src/tests/users.test.ts
import request from 'supertest';
import { app } from '../app';
import { prisma } from '../db';
describe('Users API', () => {
afterAll(() => prisma.$disconnect());
it('POST /api/users — creates user', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(201);
expect(res.body).toHaveProperty('id');
expect(res.body.email).toBe('test@example.com');
});
it('POST /api/users — rejects invalid email', async () => {
await request(app)
.post('/api/users')
.send({ email: 'not-an-email', name: 'Test' })
.expect(400);
});
it('GET /api/users/:id — returns 404 for missing user', async () => {
await request(app)
.get('/api/users/nonexistent')
.expect(404);
});
});
```
---
## Common Pitfalls
1. **Forgetting `asyncHandler`.** Unhandled promise rejections crash the process. Wrap every async route handler.
2. **Error handler not last.** Express error handlers must have 4 parameters `(err, req, res, next)` and must be registered after all routes.
3. **Not calling `next()`.** Middleware that doesn't call `next()` or send a response will hang the request.
4. **Mutating `req.body` without validation.** Always validate before trusting input. Use Zod or Joi middleware.
5. **Hardcoding CORS origin.** Use environment variables for allowed origins. Never use `cors({ origin: '*' })` in production.
6. **Missing `helmet()`.** Always use helmet for security headers. It's one line and prevents common attacks.
7. **Not limiting body size.** Use `json({ limit: '10kb' })` to prevent denial-of-service via large payloads.
8. **Using `express.static` for uploads.** Serve user uploads from a CDN or S3, not from the Express process.
---
## Related Skills
- `nestjs` — If you need DI, decorators, and modules, use NestJS instead of raw Express
- `openapi` — OpenAPI spec design for Express APIs (use `swagger-jsdoc` + `swagger-ui-express`)
- `typescript` — TypeScript patterns (Express is typed via `@types/express`)
- `docker` — Containerizing Express apps
- `authentication` — JWT / OAuth2 patterns (framework-agnostic)
- `jest` — Testing Express with Jest + supertest