`bash
mkdir logistics-api && cd logistics-api
npm init -y
npm install express cors helmet morgan dotenv
npm install -D typescript @types/node @types/express ts-node nodemon
### Step 2: Entry Point Architecture
The server entry point should never contain business logic or route definitions. Its sole responsibility is configuration, middleware registration, and lifecycle management.
```typescript
// src/server.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { config } from './config/env';
import { initializeRoutes } from './routes';
import { errorHandler } from './middleware/errorHandler';
import { gracefulShutdown } from './utils/lifecycle';
const app = express();
// Core middleware stack (order matters)
app.use(helmet());
app.use(cors({ origin: config.allowedOrigins }));
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Static assets
app.use('/assets', express.static('public'));
// Route registration
initializeRoutes(app);
// Global error handler (must be last)
app.use(errorHandler);
const startServer = async () => {
try {
const server = app.listen(config.port, () => {
console.log(`[Server] Listening on port ${config.port}`);
});
gracefulShutdown(server);
} catch (error) {
console.error('[Server] Failed to start:', error);
process.exit(1);
}
};
startServer();
Architecture Rationale:
helmet() and cors() are applied first to secure headers and control cross-origin requests before any business logic executes.
morgan provides structured request logging without polluting route handlers.
- Body parsing middleware is explicitly sized (
limit: '10mb') to prevent denial-of-service via oversized payloads.
- Error handling is centralized. Express requires error middleware to have four parameters
(err, req, res, next) and be registered after all routes.
Step 3: Modular Routing Strategy
Routes should be grouped by domain and mounted at versioned prefixes. This prevents namespace collisions and enables independent testing.
// src/routes/index.ts
import { Express } from 'express';
import { shipmentRouter } from './shipmentRouter';
import { warehouseRouter } from './warehouseRouter';
export const initializeRoutes = (app: Express) => {
app.use('/api/v1/shipments', shipmentRouter);
app.use('/api/v1/warehouses', warehouseRouter);
// Fallback for undefined routes
app.use('*', (req, res) => {
res.status(404).json({ error: 'Route not found', path: req.originalUrl });
});
};
// src/routes/shipmentRouter.ts
import { Router } from 'express';
import { validateShipmentPayload } from '../middleware/validation';
import { createShipment, getShipmentById } from '../controllers/shipmentController';
export const shipmentRouter = Router();
shipmentRouter.post('/', validateShipmentPayload, createShipment);
shipmentRouter.get('/:id', getShipmentById);
Why this structure works:
express.Router() creates isolated middleware stacks. Validation, authentication, and rate limiting can be applied per-route group without affecting others.
- Controllers remain pure functions. They receive validated data and return responses, making them trivial to unit test.
- Versioning (
/api/v1/) prevents breaking changes when contract updates occur.
Step 4: Middleware Pipeline Design
Middleware should follow a strict responsibility chain. Each layer must either pass control (next()), respond, or throw.
// src/middleware/validation.ts
import { Request, Response, NextFunction } from 'express';
export const validateShipmentPayload = (req: Request, res: Response, next: NextFunction) => {
const { origin, destination, weight } = req.body;
if (!origin || !destination || typeof weight !== 'number' || weight <= 0) {
return res.status(400).json({ error: 'Invalid shipment payload' });
}
next();
};
Key Insight: Validation middleware runs before controllers. This guarantees that business logic only executes against structurally sound data, reducing defensive coding inside controllers and improving performance.
Pitfall Guide
1. Middleware Execution Order Blindness
Explanation: Express processes middleware sequentially. Placing body parsing after route handlers, or authentication after business logic, causes silent failures or security gaps.
Fix: Always structure the stack as: Security → Logging → Parsing → Authentication → Routes → Error Handler. Document the order in architecture reviews.
2. Blocking the Event Loop
Explanation: Synchronous operations (heavy JSON parsing, regex, file I/O without streams) inside middleware or routes freeze the entire Node.js process, causing request timeouts under load.
Fix: Offload CPU-intensive tasks to worker threads or external queues. Use asynchronous I/O and streaming APIs. Profile with clinic.js or 0x to detect blocking.
3. Hardcoded Configuration
Explanation: Embedding ports, database URIs, or API keys directly in source files breaks environment isolation and exposes secrets in version control.
Fix: Use dotenv for local development and environment variables for production. Validate configuration at startup with a schema validator like zod or joi.
4. Missing Error Boundary
Explanation: Unhandled promise rejections or thrown errors inside async route handlers crash the process or leave requests hanging.
Fix: Wrap async routes in a try/catch or use a wrapper like express-async-handler. Always register a four-parameter error middleware as the final route.
5. Over-Reliance on Global State
Explanation: Storing request-specific data (user sessions, request IDs, tenant context) in module-level variables causes cross-request contamination in concurrent environments.
Fix: Attach request-scoped data to req object or use AsyncLocalStorage for context propagation. Never mutate shared module state.
6. Mixing Routing and Business Logic
Explanation: Embedding database queries, third-party API calls, or complex calculations directly in route handlers creates tight coupling and makes testing impossible.
Fix: Enforce a strict controller-service-repository pattern. Routes handle HTTP contracts, controllers orchestrate, services contain business rules, repositories manage persistence.
7. Ignoring Graceful Shutdown
Explanation: Sending SIGTERM to a Node process without cleanup drops active connections, corrupts in-flight transactions, and causes data loss during deployments.
Fix: Listen for SIGTERM/SIGINT, stop accepting new connections, drain active requests, close database pools, and exit cleanly. Implement health check endpoints for orchestrators.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal Microservice (< 5 endpoints) | Monolithic app.js with inline routers | Reduces boilerplate, faster iteration | Low infrastructure, higher maintenance as scope grows |
| Public API / Multi-tenant Platform | Modular Router + Service Layer + Validation Middleware | Enforces contracts, isolates failures, scales teams | Moderate initial setup, significantly lower long-term maintenance |
| Real-time / High-Concurrency Gateway | Express + WebSocket adapter + Connection Pooling | Handles persistent connections efficiently | Higher memory footprint, requires load balancing |
| Legacy Migration | Express with http-proxy-middleware + gradual route extraction | Minimizes downtime, allows incremental refactoring | Temporary dual-maintenance cost, accelerates modernization |
Configuration Template
// package.json
{
"name": "logistics-api",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "jest --coverage",
"lint": "eslint src/ --ext .ts"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"helmet": "^7.1.0",
"morgan": "^1.10.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/node": "^20.10.0",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.2"
}
}
// src/config/env.ts
import dotenv from 'dotenv';
dotenv.config();
export const config = {
port: parseInt(process.env.PORT || '3000', 10),
nodeEnv: process.env.NODE_ENV || 'development',
allowedOrigins: (process.env.ALLOWED_ORIGINS || 'http://localhost:3000').split(','),
logLevel: process.env.LOG_LEVEL || 'combined'
};
if (!config.nodeEnv.match(/^(development|staging|production)$/)) {
throw new Error('Invalid NODE_ENV value');
}
Quick Start Guide
- Clone & Install: Run
npm install to resolve dependencies and TypeScript definitions.
- Configure Environment: Create a
.env file with PORT=3000, NODE_ENV=development, and ALLOWED_ORIGINS=http://localhost:3000.
- Launch Development Server: Execute
npm run dev. The server starts with hot-reloading via nodemon and TypeScript compilation via ts-node.
- Verify Health: Send
GET http://localhost:3000/health to confirm middleware stack and routing are operational.
- Add First Route: Create a controller, register it in the router module, and mount it under
/api/v1/. Test with curl or Postman before committing.