mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-10 12:14:57 +03:00
12 KiB
12 KiB
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
nestjsskill (NestJS wraps Express but has its own patterns) - FastAPI / Django — use the
fastapiordjangoskill - Frontend — use
reactornextjs - 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
// 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 };
// src/server.ts
import { app } from './app';
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`Listening on :${PORT}`));
Routing
Router pattern
// 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
// 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
// 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
// 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
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
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
// 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);
}
};
}
// 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)
// 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
// 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
// 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);
});
// 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
// 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
- Forgetting
asyncHandler. Unhandled promise rejections crash the process. Wrap every async route handler. - Error handler not last. Express error handlers must have 4 parameters
(err, req, res, next)and must be registered after all routes. - Not calling
next(). Middleware that doesn't callnext()or send a response will hang the request. - Mutating
req.bodywithout validation. Always validate before trusting input. Use Zod or Joi middleware. - Hardcoding CORS origin. Use environment variables for allowed origins. Never use
cors({ origin: '*' })in production. - Missing
helmet(). Always use helmet for security headers. It's one line and prevents common attacks. - Not limiting body size. Use
json({ limit: '10kb' })to prevent denial-of-service via large payloads. - Using
express.staticfor 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 Expressopenapi— OpenAPI spec design for Express APIs (useswagger-jsdoc+swagger-ui-express)typescript— TypeScript patterns (Express is typed via@types/express)docker— Containerizing Express appsauthentication— JWT / OAuth2 patterns (framework-agnostic)jest— Testing Express with Jest + supertest