xpress cors helmet morgan dotenv zod
npm install -D nodemon
### 2. Application Configuration
Configure the middleware chain in `app.js`. The order of middleware is critical; security and parsing must occur before routing.
```javascript
// src/app.js
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { inventoryRouter } from './routes/inventory.routes.js';
import { globalErrorHandler } from './middleware/error.handler.js';
const app = express();
// Security and Parsing Middleware
app.use(helmet()); // Sets secure HTTP headers
app.use(cors({ origin: process.env.ALLOWED_ORIGINS })); // Restrict CORS
app.use(morgan('combined')); // Structured logging
app.use(express.json({ limit: '10kb' })); // Parse JSON with size limit
// API Versioning and Routing
app.use('/api/v1/inventory', inventoryRouter);
// Global Error Handler (Must be last)
app.use(globalErrorHandler);
export { app };
Rationale:
- Helmet: Automatically sets headers like
X-Content-Type-Options and Strict-Transport-Security, mitigating common web vulnerabilities.
- CORS Configuration: Never use
cors() without options in production. Explicitly define allowed origins to prevent unauthorized cross-site requests.
- JSON Limit: Setting a limit prevents denial-of-service attacks via massive payloads.
- Error Handler Placement: Express executes middleware sequentially. The error handler must be defined after all routes to catch errors thrown downstream.
3. Resource Modeling and Routing
REST resources should be nouns, pluralized. Avoid verbs in URIs. The HTTP method conveys the action.
// src/routes/inventory.routes.js
import { Router } from 'express';
import { inventoryController } from '../controllers/inventory.controller.js';
import { validateRequest } from '../middleware/validation.middleware.js';
import { createItemSchema } from '../schemas/inventory.schema.js';
const router = Router();
// GET /api/v1/inventory - List resources
router.get('/', inventoryController.list);
// GET /api/v1/inventory/:id - Read single resource
router.get('/:id', inventoryController.getById);
// POST /api/v1/inventory - Create resource
router.post('/', validateRequest(createItemSchema), inventoryController.create);
// PUT /api/v1/inventory/:id - Replace resource (Idempotent)
router.put('/:id', validateRequest(createItemSchema), inventoryController.replace);
// PATCH /api/v1/inventory/:id - Partial update
router.patch('/:id', inventoryController.update);
// DELETE /api/v1/inventory/:id - Remove resource
router.delete('/:id', inventoryController.remove);
export { router as inventoryRouter };
Rationale:
- Router Module: Encapsulates routes for a specific resource, improving modularity.
- Validation Middleware: Injecting validation before the controller ensures that business logic only processes valid data.
- PUT vs PATCH:
PUT implies a full replacement and must be idempotent. PATCH is for partial updates. Distinguishing these clarifies client expectations.
4. Controller and Service Separation
Controllers should handle HTTP concerns only. Business logic belongs in services.
// src/controllers/inventory.controller.js
import { inventoryService } from '../services/inventory.service.js';
import { ApiError } from '../middleware/error.handler.js';
export const inventoryController = {
async list(req, res, next) {
try {
const { page = 1, limit = 20, filter } = req.query;
const result = await inventoryService.findAll({
page: Number(page),
limit: Number(limit),
filter
});
res.status(200).json(result);
} catch (err) {
next(err);
}
},
async create(req, res, next) {
try {
const newItem = await inventoryService.create(req.body);
res.status(201).json({ data: newItem });
} catch (err) {
next(err);
}
},
async remove(req, res, next) {
try {
const affected = await inventoryService.remove(req.params.id);
if (affected === 0) {
throw new ApiError(404, 'Resource not found');
}
res.status(204).send();
} catch (err) {
next(err);
}
}
};
Rationale:
- Pagination: The
list method accepts query parameters for pagination. Returning unbounded datasets is a common production failure.
- Status Codes:
201 for creation, 204 for successful deletion with no body. This aligns with HTTP standards.
- Error Forwarding: Using
next(err) delegates error handling to the global middleware, keeping controllers clean.
5. Global Error Handling
A unified error handler ensures consistent error responses and prevents stack trace leakage.
// src/middleware/error.handler.js
export class ApiError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
export const globalErrorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal Server Error';
// Log error details internally
console.error(`[ERROR] ${statusCode} - ${err.message}`);
res.status(statusCode).json({
status: 'error',
statusCode,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
Pitfall Guide
1. Verb Pollution in URIs
Explanation: Developers often create endpoints like POST /createUser or GET /getAllProducts. This violates REST constraints by embedding actions in the resource identifier.
Fix: Use nouns for resources and HTTP methods for actions. Map POST /users for creation and GET /users for retrieval.
2. Status Code Ambiguity
Explanation: Returning 200 OK for both successful operations and errors forces the client to parse the response body to determine success. This breaks standard HTTP tooling and caching.
Fix: Use precise status codes. 200 for success, 201 for creation, 400 for validation errors, 404 for missing resources, and 500 for server failures.
3. Ignoring Idempotency
Explanation: Treating POST as idempotent or PUT as non-idempotent leads to data corruption. If a client retries a PUT request, the resource state must remain unchanged after the first application.
Fix: Ensure PUT operations replace the resource entirely based on the payload. Use POST only for non-idempotent actions like creating a new order or processing a payment.
4. The "God Controller" Anti-pattern
Explanation: Embedding database queries, validation, and business rules directly in route handlers creates tightly coupled code that is difficult to test and maintain.
Fix: Adopt a layered architecture. Routes handle transport, controllers manage request/response mapping, services contain business logic, and repositories handle data access.
5. Leaking Internal Errors
Explanation: Returning stack traces or database error messages to the client exposes internal implementation details and security vulnerabilities.
Fix: Implement a global error handler that returns generic messages to clients in production while logging detailed errors internally. Use the ApiError pattern to distinguish operational errors from programming bugs.
6. Missing Pagination and Filtering
Explanation: Returning all records for a collection endpoint causes performance degradation and memory exhaustion as data grows.
Fix: Always implement pagination (limit, offset or cursor-based) and filtering for collection endpoints. Enforce maximum limits to prevent abuse.
7. Inconsistent Response Envelopes
Explanation: Some endpoints return an array directly, while others return { data: [...] }. This inconsistency forces clients to write conditional parsing logic.
Fix: Standardize the response envelope. Use a consistent structure like { data: ..., meta: { pagination: ... } } across all endpoints.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Input Validation | Zod / Joi | Schema-based validation reduces boilerplate and improves type safety. | Low (Dev time) |
| Error Handling | Global Middleware | Centralizes error logic and ensures consistent client responses. | Low (Refactor) |
| Logging | Winston / Pino | Structured logs enable better monitoring and debugging in production. | Low (Setup) |
| Security Headers | Helmet | Automates security best practices; reduces risk of misconfiguration. | None (Package) |
| API Versioning | URL Path (/v1) | Simplest approach for clients; clear contract boundaries. | Low (Routing) |
Configuration Template
.env.example
PORT=3000
NODE_ENV=development
ALLOWED_ORIGINS=http://localhost:3000,https://app.example.com
DB_URI=mongodb://localhost:27017/inventory_db
LOG_LEVEL=info
src/server.js
import { app } from './app.js';
import dotenv from 'dotenv';
dotenv.config();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT} in ${process.env.NODE_ENV} mode`);
});
Quick Start Guide
- Initialize Project: Run
npm init -y and install dependencies: npm install express cors helmet morgan dotenv zod.
- Create Structure: Set up
src/ with app.js, server.js, routes/, controllers/, services/, and middleware/ directories.
- Configure App: Copy the
app.js template, adding middleware and route imports.
- Define Routes: Create resource routes using plural nouns and standard HTTP methods.
- Run Server: Execute
node src/server.js or use nodemon for development. Verify endpoints using a tool like Postman or curl.