Conventions for building maintainable, secure Express.js applications covering routing structure, middleware order, error handling, and request validation.
# Express.js Conventions
## Project Structure
- Organize by feature, not by layer: `src/features/users/`, `src/features/orders/` — each contains its own router, controller, service, and types.
- Entry point `src/app.ts` creates and configures the Express app. `src/server.ts` starts the HTTP server. Keep them separate for testability.
- Register routes in `src/routes/index.ts` and mount there with version prefix: `app.use('/api/v1', routes)`.
- Controllers handle HTTP: parse request, call service, send response. No business logic in controllers.
- Services contain business logic. They are framework-agnostic and must not reference `req` or `res`.
## Router & Controller Patterns
- Create one `Router` per feature module. Use `express.Router({ strict: true })`.
- Name route handler files `*.controller.ts`. Controllers are thin — 5-15 lines per method.
- Always use `async` route handlers and wrap with an `asyncHandler` utility that calls `next(err)` on rejection.
- Never use `try/catch` in every controller — use a single async error wrapper instead.
- Use explicit HTTP methods and paths. Avoid catch-all routes except for the 404 handler.
## Middleware Order
- Strict middleware registration order (top to bottom in `app.ts`):
1. Security headers (`helmet`)
2. CORS (`cors`)
3. Request ID (`x-request-id` injection)
4. Request logging (Morgan or custom)
5. Body parsing (`express.json()`, `express.urlencoded()`)
6. Rate limiting
7. Authentication middleware (if global)
8. Route handlers
9. 404 handler
10. Global error handler
- Never register the error handler before routes — it will not catch route errors.
## Request Validation
- Validate all request input (body, params, query) with a schema library (Zod, Joi, Yup) before the controller logic.
- Create a `validate(schema)` middleware factory that validates and returns 400 with structured errors on failure.
- Use TypeScript types inferred from validation schemas — do not define separate types for the same shape.
- Reject requests with unexpected fields — use `strict()` mode in your schema.
## Error Handling
- Define a typed `AppError` class: `class AppError extends Error { constructor(message: string, public status: number, public code: string) }`.
- All errors pass through `next(err)` — never use `res.status(500).json(...)` inline in handlers.
- Global error handler (4 args: `err, req, res, next`) formats errors: `{ error: { code, message, details? } }`.
- Never expose stack traces in production responses — log them server-side, return a generic message.
- Map operational errors (validation, not found, unauthorized) to appropriate 4xx codes.
## Security
- Always use `helmet()` — it sets safe HTTP headers by default.
- Set `trust proxy` if behind a load balancer: `app.set('trust proxy', 1)`.
- Apply rate limiting per route group: stricter limits on auth routes (`/login`, `/register`).
- Sanitize all inputs. Use `express-mongo-sanitize` if using MongoDB. Parameterize all DB queries.
- Set `Content-Security-Policy`, disable `X-Powered-By` (`app.disable('x-powered-by')`).
## Response Conventions
- Use a consistent response envelope: `{ data: T }` for success, `{ error: { code, message } }` for failure.
- Always set explicit HTTP status codes — never rely on the default 200.
- Use `res.json()` — never `res.send()` for JSON data.
- For 204 No Content responses, call `res.status(204).end()` — do not send a body.
## Anti-Patterns
- Do not define route handlers inline in `app.ts` — use Router modules.
- Do not mutate `req` to pass data between middleware — use `res.locals` for request-scoped data.
- Do not use synchronous file I/O in middleware or route handlers.
- Do not disable security middleware in tests — use test-specific credentials instead.