These aren't theoretical — they're patterns from production Node.js apps that handle millions of requests.
1. Structure your project by feature, not by type
# ❌ Bad — too abstract
src/
controllers/
models/
routes/
services/
# ✅ Good — feature-based
src/
features/
auth/
auth.controller.js
auth.service.js
auth.routes.js
auth.test.js
users/
users.controller.js
users.service.js
Why? When you work on "users", all relevant code is in one place. No more hunting across 5 folders.
2. Use async/await everywhere, handle errors properly
// ❌ Wrong - unhandled promise, crashes the process
app.get('/users/:id', async (req, res) => {
const user = await UserService.findById(req.params.id);
res.json(user);
});
// ✅ Better - catch errors, pass to error middleware
app.get('/users/:id', async (req, res, next) => {
try {
const user = await UserService.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (err) {
next(err); // Let error middleware handle it
}
});
// ✅ Even better - wrap all routes (Express 5 does this natively)
// Or use a helper:
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await UserService.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
}));
3. Validate input at the edge
// Using Zod (best validation library in 2026)
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['user', 'admin', 'moderator']).default('user')
});
app.post('/users', async (req, res) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user = await UserService.create(result.data); // Type-safe!
res.status(201).json(user);
});
4. Centralized error handling
// errors.js - Custom error classes
export class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
}
}
export class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
export class ValidationError extends AppError {
constructor(message) {
super(message, 400, 'VALIDATION_ERROR');
}
}
// Error middleware (must be LAST in Express)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = {
error: {
message: err.message,
code: err.code || 'INTERNAL_ERROR',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
};
if (statusCode >= 500) {
logger.error({ err, req: { method: req.method, url: req.url } });
}
res.status(statusCode).json(response);
});
5. Use environment variables correctly
// config.js - Central config with validation
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional(),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('Invalid environment variables:', parsed.error.flatten());
process.exit(1); // Fail fast — better than mysterious errors later
}
export const config = parsed.data;
// Usage: import { config } from './config.js'
6. Logging that actually helps
// Use pino (fastest Node.js logger, outputs JSON)
import pino from 'pino';
export const logger = pino({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport: process.env.NODE_ENV !== 'production' ? {
target: 'pino-pretty',
options: { colorize: true }
} : undefined
});
// Log with context, not just messages
logger.info({ userId: user.id, action: 'login', ip: req.ip }, 'User logged in');
logger.error({ err, orderId: order.id }, 'Payment failed');
// Add request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: Date.now() - start,
});
});
next();
});
7. Handle process signals gracefully
// Graceful shutdown — finish active requests before exiting
const server = app.listen(config.PORT);
async function shutdown(signal) {
logger.info({ signal }, 'Shutting down...');
server.close(async () => {
try {
await db.disconnect();
await redis?.quit();
logger.info('Graceful shutdown complete');
process.exit(0);
} catch (err) {
logger.error({ err }, 'Error during shutdown');
process.exit(1);
}
});
// Force close after 30s
setTimeout(() => {
logger.error('Forced shutdown');
process.exit(1);
}, 30000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('unhandledRejection', (reason) => {
logger.error({ reason }, 'Unhandled Promise rejection');
shutdown('unhandledRejection');
});
8. Rate limiting and security headers
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
// Security headers (XSS, clickjacking, MIME sniffing)
app.use(helmet());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later' }
});
app.use('/api', limiter);
// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
message: { error: 'Too many login attempts' }
});
app.use('/api/auth/login', authLimiter);
The checklist
- [ ] Feature-based project structure
- [ ] Async error handling everywhere (try/catch or asyncHandler)
- [ ] Input validation with Zod or Joi
- [ ] Centralized error middleware
- [ ] Environment variables validated on startup
- [ ] Structured logging (pino or winston)
- [ ] Graceful shutdown handling
- [ ] Rate limiting + security headers (helmet)
What pattern do you use that's not on this list? Share it in the comments!
Top comments (0)