| name | express-server |
| description | Express server patterns with TypeScript, InversifyJS, and Socket.IO. Use when creating or modifying HTTP endpoints, middleware, route handlers, CORS config, graceful shutdown, or writing supertest integration tests.
|
Express Server Skill
Stack: Express 4.x, TypeScript, ESM ("type": "module"), InversifyJS DI, Socket.IO, cors, helmet.
The server is a thin HTTP layer — business logic lives in injected services, not in route files.
Project Structure
src/
server/
app.ts # Express factory — creates and configures app, no listen()
server.ts # Entry point — binds port, handles SIGTERM
routes/ # One file per resource, returns express.Router
middleware/ # Custom middleware (auth, logging, validation)
errors/ # AppError class, error handler middleware
Middleware Ordering
Register in this exact order — order is critical:
app.use(helmet());
app.use(cors(corsOptions));
app.use(express.json({ limit: '1mb' }));
app.use(rateLimiter);
app.use('/static', express.static(staticDir, { maxAge: '1d' }));
app.use('/health', healthRouter);
app.use('/api', apiRouter);
app.use(errorHandler);
Async Error Handling (Express 4)
Express 4 does not catch async errors automatically. Wrap handlers or use express-async-errors.
const asyncHandler =
(fn: RequestHandler): RequestHandler =>
(req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
import 'express-async-errors';
Error middleware — must have exactly four parameters:
app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
const status = err instanceof AppError ? err.status : 500;
const message = err instanceof AppError ? err.message : 'Internal Server Error';
res.status(status).json({ error: message });
});
Security
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
app.use(helmet());
const corsOptions: CorsOptions = {
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [],
credentials: true,
};
const limiter = rateLimit({ windowMs: 60_000, max: 100 });
app.use('/api', limiter);
Never use origin: '*' in production when credentials: true.
Health Check
healthRouter.get('/', (_req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
Kubernetes pattern: return 503 during shutdown to drain load balancer traffic before process exits.
Graceful Shutdown
const httpServer = app.listen(port);
const shutdown = () => {
httpServer.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10_000).unref();
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
For Socket.IO: call io.close() inside shutdown before httpServer.close().
InversifyJS Integration
Resolve services from the container inside route factories — never new:
export function createApiRouter(container: Container): Router {
const router = Router();
const svc = container.get(MyService);
router.get('/', asyncHandler(async (req, res) => {
res.json(await svc.list());
}));
return router;
}
Testing with Supertest
import request from 'supertest';
import { buildApp } from '../app.js';
const app = buildApp(testContainer);
it('GET /health returns 200', async () => {
await request(app).get('/health').expect(200);
});
Bind a real or stub container — avoid mocking the HTTP layer itself.
Common Pitfalls
- Forgetting
next(err) in Express 4 async handlers causes silent hangs.
- Registering error middleware with three params — it silently becomes a regular middleware.
- Calling
app.listen() inside the app factory breaks supertest (port conflicts).
- Setting
origin: '*' with credentials: true is rejected by browsers.
- Static middleware before
helmet() can serve files without security headers.
- Not calling
io.close() on shutdown leaves Socket.IO connections hanging.
Sources: