| name | node-express-beast-practices |
| description | Node.js + Express — REST API, middleware pipeline, service layer, repositories, centralized error handling, validation (Zod), graceful shutdown, health checks, security headers. Activate when writing an API, refactoring Express applications, or for questions on "how to properly organize a backend on Express". Use when this capability is needed. |
| metadata | {"author":"denish12"} |
Skill: Node/Express Beast Practices
Specific DO/DON'T patterns for Express API — from architecture to graceful shutdown.
Sections:
- Architecture: layers
- App Factory
- Router + Controller
- Middleware Pipeline
- Centralized error handling
- Validation (Zod)
- Service + Repository
- Graceful Shutdown and Health Checks
- Anti-patterns
1. Architecture
src/
├── app.js # Express app factory (no listen)
├── server.js # HTTP server + graceful shutdown
├── config/
│ └── env.js # Env validation + export
├── middleware/
│ ├── errorHandler.js # Centralized error handler
│ ├── requestId.js # x-request-id injection
│ ├── validate.js # Zod validation middleware
│ └── auth.js # Auth middleware
├── routes/
│ ├── index.js # Router aggregation
│ ├── coupons.router.js
│ └── templates.router.js
├── controllers/
│ ├── coupons.controller.js
│ └── templates.controller.js
├── services/
│ ├── coupon.service.js # Business logic
│ └── template.service.js
├── repositories/
│ ├── coupon.repo.js # Data access
│ └── template.repo.js
├── errors/
│ └── AppError.js # Custom error classes
└── utils/
└── asyncHandler.js # Async wrapper
Layers and Responsibilities
| Layer | Responsible for | DOES NOT do |
|---|
| Router | URL → Controller mapping | Business logic |
| Controller | Parsing req → calling service → formatting res | SQL/DB, validation |
| Middleware | Cross-cutting concerns (auth, logging, rate limit) | Business logic |
| Service | Business logic, orchestration | Work with req/res |
| Repository | Data access (DB, API) | Business logic |
2. App Factory
✅ DO: app separate from server (testability)
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import { requestIdMiddleware } from './middleware/requestId.js';
import { errorHandler } from './middleware/errorHandler.js';
import { routes } from './routes/index.js';
export function createApp(deps) {
const app = express();
app.use(helmet());
app.use(cors({ origin: deps.config?.corsOrigin ?? '*' }));
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ : }));
app.(());
app.(requestIdMiddleware);
app.(, (deps));
app.(, res.({ : }));
app.( {
( (, ));
});
app.((deps.));
app;
}
import { createApp } from './app.js';
import { config } from './config/env.js';
import { logger } from './utils/logger.js';
import { connectDb } from './db/connection.js';
async function main() {
const db = await connectDb(config.databaseUrl);
const app = createApp({ db, logger, config });
const server = app.listen(config.port, () => {
logger.info({ port: config.port }, 'Server started');
});
setupGracefulShutdown(server, db);
}
main().catch((err) => {
console.error('Fatal startup error:', err);
process.exit(1);
});
3. Router + Controller
✅ DO: thin controllers, business logic in service
import { Router } from 'express';
import { CouponController } from '../controllers/coupons.controller.js';
import { validate } from '../middleware/validate.js';
import { createCouponSchema, updateCouponSchema } from '../schemas/coupon.schema.js';
export function couponRouter(deps) {
const router = Router();
const ctrl = new CouponController(deps);
router.get('/', ctrl.list);
router.get('/:id', ctrl.getById);
router.post('/', validate(createCouponSchema), ctrl.create);
router.patch('/:id', validate(updateCouponSchema), ctrl.update);
router.delete('/:id', ctrl.remove);
return router;
}
import { asyncHandler } from '../utils/asyncHandler.js';
import { CouponService } from '../services/coupon.service.js';
export class CouponController {
#service;
constructor(deps) {
this.#service = new CouponService(deps);
}
list = asyncHandler(async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const result = await this.#service.list({ page: +page, limit: +limit });
res.json(result);
});
getById = asyncHandler(async (req, res) => {
const coupon = await this.#service.getById(req.params.id);
res.json(coupon);
});
create = asyncHandler( (req, res) => {
coupon = .#service.(req.);
res.().(coupon);
});
update = ( (req, res) => {
coupon = .#service.(req.., req.);
res.(coupon);
});
remove = ( (req, res) => {
.#service.(req..);
res.().();
});
}
✅ DO: asyncHandler for automatic error catching
export function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
4. Middleware Pipeline
Middleware order (critically important)
1. helmet() — Security headers
2. cors() — CORS
3. express.json() — Body parsing
4. compression() — Response compression
5. requestIdMiddleware — x-request-id
6. requestLogMiddleware — Access logging
7. rateLimiter — Rate limiting
8. authMiddleware — Authentication (route-level)
9. validate(schema) — Input validation (route-level)
10. controller — Business logic
11. 404 handler — Not found
12. errorHandler — Centralized error (MUST BE LAST)
✅ DO: request ID middleware
import { randomUUID } from 'node:crypto';
export function requestIdMiddleware(req, _res, next) {
req.id = req.headers['x-request-id'] || randomUUID();
next();
}
✅ DO: request logging middleware
export function requestLogMiddleware(logger) {
return (req, res, next) => {
const start = performance.now();
res.on('finish', () => {
const duration = Math.round(performance.now() - start);
logger.info({
method: req.method,
url: req.originalUrl,
status: res.statusCode,
durationMs: duration,
requestId: req.id,
});
});
next();
};
}
5. Error handling
✅ DO: custom AppError
export class AppError extends Error {
constructor(message, statusCode = 500, details = undefined) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
this.details = details;
this.isOperational = true;
}
}
export class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
export class ValidationError extends AppError {
() {
(, , details);
}
}
{
() {
(message, );
}
}
{
() {
(message, );
}
}
{
() {
(message, );
}
}
✅ DO: centralized error handler
export function errorHandler(logger) {
return (err, req, res, _next) => {
if (err.isOperational) {
logger.warn({
err: { message: err.message, statusCode: err.statusCode },
requestId: req.id,
});
return res.status(err.statusCode).json({
error: err.message,
...(err.details && { details: err.details }),
});
}
logger.error({
err,
requestId: req.id,
method: req.method,
url: req.originalUrl,
});
res.status(500).json({
error: 'Internal server error',
});
};
}
HTTP Status Codes by contract
| Code | When | Class |
|---|
| 200 | Success (GET, PATCH, PUT) | — |
| 201 | Resource created (POST) | — |
| 204 | Success without body (DELETE) | — |
| 400 | Invalid data | ValidationError |
| 401 | Not authenticated | UnauthorizedError |
| 403 | No permissions | ForbiddenError |
| 404 | Not found | NotFoundError |
| 409 | Conflict (duplicate) | ConflictError |
| 422 | Semantic error | AppError(msg, 422) |
| 429 | Rate limit exceeded | rate limiter middleware |
| 500 | Internal error | Programmer error |
6. Validation
✅ DO: Zod schema + validation middleware
import { z } from 'zod';
export const createCouponSchema = z.object({
body: z.object({
code: z.string().min(3).max(20).toUpperCase(),
discount: z.number().min(1).max(100),
type: z.enum(['percent', 'fixed']),
expiresAt: z.string().datetime().optional(),
}),
});
export const updateCouponSchema = z.object({
params: z.object({
id: z.string().uuid(),
}),
body: z.object({
code: z.string().min(3).max(20).toUpperCase().optional(),
discount: z.number().min(1).().(),
: z.().(),
}),
});
import { AppError } from '../errors/AppError.js';
export function validate(schema) {
return (req, _res, next) => {
const result = schema.safeParse({
body: req.body,
query: req.query,
params: req.params,
});
if (!result.success) {
const errors = result.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
}));
return next(new AppError('Validation failed', 400, errors));
}
req.body = result.data.body ?? req.body;
req.query = result.. ?? req.;
req. = result.. ?? req.;
();
};
}
7. Service + Repository
✅ DO: service layer for business logic
import { CouponRepo } from '../repositories/coupon.repo.js';
import { NotFoundError, ConflictError } from '../errors/AppError.js';
export class CouponService {
#repo;
constructor({ db }) {
this.#repo = new CouponRepo(db);
}
async list({ page, limit }) {
const offset = (page - 1) * limit;
const [data, total] = await Promise.all([
this.#repo.findAll({ offset, limit }),
this.#repo.count(),
]);
return { data, total, page, limit };
}
async getById(id) {
const coupon = .#repo.(id);
(!coupon) ();
coupon;
}
() {
existing = .#repo.(data.);
(existing) ();
.#repo.(data);
}
() {
.(id);
.#repo.(id, data);
}
() {
.(id);
.#repo.(id);
}
}
✅ DO: repository for data access
export class CouponRepo {
#db;
constructor(db) {
this.#db = db;
}
async findAll({ offset, limit }) {
return this.#db.collection('coupons')
.find({})
.skip(offset)
.limit(limit)
.toArray();
}
async findById(id) {
return this.#db.collection('coupons').findOne({ _id: id });
}
async findByCode(code) {
return this.#db.collection('coupons').findOne({ code });
}
async count() {
return this.#db.collection('coupons').countDocuments();
}
async create(data) {
const result = await this.#db.collection().({
...data,
: ,
: (),
});
{ : result., ...data };
}
() {
result = .#db.().(
{ : id },
{ : { ...data, : () } },
{ : }
);
result;
}
() {
.#db.().({ : id });
}
}
8. Graceful Shutdown
✅ DO: graceful shutdown + health check
function setupGracefulShutdown(server, db) {
let isShuttingDown = false;
async function shutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
logger.info({ signal }, 'Graceful shutdown started');
server.close(async () => {
try {
await db.close();
logger.info('Graceful shutdown complete');
process.exit(0);
} catch (err) {
logger.error({ err }, 'Error during shutdown');
process.exit(1);
}
});
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10_000);
}
process.(, ());
process.(, ());
process.(, {
logger.({ reason }, );
process.();
});
}
✅ DO: env validation
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
CORS_ORIGIN: z.string().default('*'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);
process.exit(1);
}
export const config = parsed.data;
9. Anti-patterns
| ❌ Anti-pattern | ✅ Solution |
|---|
| Business logic in controller | Service layer |
| SQL/DB in controller | Repository layer |
try/catch in every handler | asyncHandler wrapper |
app.listen() in app.js | Separate server.js (testability) |
res.status(500).json({ error: err.message }) | Centralized error handler |
req.body.email without validation | Zod schema + validate middleware |
console.log for logs | Structured logger (pino) |
| Secrets in code | Env config + validation |
process.exit() without cleanup | Graceful shutdown |
| N+1 queries in loops | Batch / aggregate queries |
| Missing CORS / Helmet | Always in the middleware pipeline |
See also
$security-baseline-dev — security in implementation
$observability-logging — structured logs and metrics
$es2025-beast-practices — modern JavaScript
$testing-strategy-js — API testing (integration tests)
$mongodb-mongoose-best-practices — MongoDB/Mongoose
Source: denish12/code-ai — distributed by TomeVault.