Express Basics
Express is an unopinionated, minimalist routing and middleware framework that abstracts raw node:http servers into structured request pipelines.
Incoming Request ──▶ [ express.json() ] ──▶ [ Custom Middleware ] ──▶ [ Route Handler ] ──▶ Response
Application Setup & Request/Response Cycle
An Express application manages an internal stack of handler functions. Every incoming HTTP request flows sequentially through this stack until a terminal handler writes headers and calls res.send(), res.json(), or res.end().
JavaScript
import express from 'express';
const app = express();
// Body parsers: transforms raw incoming byte streams into JavaScript objects
app.use(express.json({ limit: '10kb' })); // Parse application/json
app.use(express.urlencoded({ extended: true })); // Parse application/x-www-form-urlencoded
// Basic Route Definition
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Parsing URL Parameters, Query Strings, and Bodies
- Route Parameters (
req.params): Named URL segments used to identify a specific resource. - Query Strings (
req.query): Key-value pairs appended after?in the URL, used for filtering, pagination, and sorting. - Request Body (
req.body): Payload submitted in POST/PUT/PATCH requests (populated by body-parsing middleware). - Headers (
req.headers): Access headers likeauthorizationorcontent-type.
JavaScript
// GET /api/v1/products/prod_99?sort=desc&limit=5
app.get('/api/v1/products/:id', (req, res) => {
const { id } = req.params; // 'prod_99'
const { sort, limit = '10' } = req.query; // sort: 'desc', limit: '5'
res.status(200).json({ productId: id, sort, limit: parseInt(limit, 10) });
});
// POST /api/v1/products
app.post('/api/v1/products', (req, res) => {
const { name, price } = req.body; // Extracted from parsed JSON body
res.status(201).json({ created: { name, price } });
});
Middleware Architecture
A middleware function has access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.
JavaScript
function middleware(req, res, next) {
// 1. Execute custom logic
// 2. Mutate req or res objects if needed
// 3. Pass control to the next middleware:
next();
// ...or terminate the request immediately:
// res.status(403).json({ error: 'Forbidden' });
}
The 5 Types of Express Middleware
- Application-Level: Bound directly to an instance of
appusingapp.use()orapp.METHOD(). Runs on every matching path. - Router-Level: Bound to an instance of
express.Router(). Confines middleware execution to a specific domain or resource subset. - Built-in: Bundled with Express:
express.json(),express.urlencoded(),express.static('public'). - Third-Party: Maintained in the npm ecosystem:
cors: Configures Cross-Origin Resource Sharing headers.morgan: HTTP request logger.helmet: Sets secure HTTP response headers.
JavaScript
import cors from 'cors';
import morgan from 'morgan';
// Application-level third-party middleware
app.use(cors({ origin: 'https://trusted-client.com', methods: ['GET', 'POST'] }));
app.use(morgan('combined'));
- Error-Handling Middleware: Defined with strictly four arguments:
(err, req, res, next). Express identifies this signature viafn.length === 4.
JavaScript
// Centralized Error-Handling Middleware (Must be declared AFTER all routes)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal Server Error';
console.error(`[Error] ${req.method} ${req.url}:`, err);
res.status(statusCode).json({
status: 'error',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
});
Router-Level Isolation
JavaScript
// routes/users.routes.js
import { Router } from 'express';
const router = Router();
// Router-specific middleware: applies only to routes mounted on this router
router.use((req, res, next) => {
console.log(`User Router access: ${req.ip}`);
next();
});
router.get('/', (req, res) => res.json({ users: [] }));
router.get('/:id', (req, res) => res.json({ user: req.params.id }));
export default router;
// app.js
import userRouter from './routes/users.routes.js';
app.use('/api/v1/users', userRouter);
REST API Design Principles
Representational State Transfer (REST) structures APIs around resources (nouns, not verbs) manipulated using standard HTTP protocols.
HTTP Verbs & Standard Status Codes
| Verb | Semantics | Success Code | Idempotent | Safe |
| GET | Read resource(s) | 200 OK | Yes | Yes |
| POST | Create resource or execute arbitrary action | 201 Created | No | No |
| PUT | Replace a resource completely | 200 OK / 204 No Content | Yes | No |
| PATCH | Apply partial modifications to a resource | 200 OK | No (in spec) | No |
| DELETE | Remove a resource | 200 OK / 204 No Content | Yes | No |
- Safe: Does not modify server state (read-only).
- Idempotent: Making multiple identical requests results in the same server state as a single request.
DELETE /users/1is idempotent (the record remains gone whether called once or five times), whereasPOST /userscreates a new record on each invocation.
Common HTTP Status Codes
- 2xx Success:
200 OK,201 Created,204 No Content - 3xx Redirection:
301 Moved Permanently,304 Not Modified(caching) - 4xx Client Error:
400 Bad Request,401 Unauthorized(no/invalid auth token),403 Forbidden(authenticated, but insufficient rights),404 Not Found,409 Conflict(e.g., duplicate unique field),422 Unprocessable Entity(validation errors),429 Too Many Requests - 5xx Server Error:
500 Internal Server Error,502 Bad Gateway,503 Service Unavailable
Resource URI Conventions & Versioning
- Use plural nouns:
/api/v1/orders(not/api/v1/getAllOrders). - Model relationships hierarchically:
/api/v1/users/42/orders. - Maintain backwards compatibility through URI versioning:
/api/v1/...vs./api/v2/....
Layered Architecture (Controller-Service-Repository)
Separating concerns isolates HTTP protocol details from core business logic and persistence layers, simplifying automated testing and maintenance.
Request ──▶ Controller (HTTP/Parsing) ──▶ Service (Business Logic) ──▶ Repository (Database)
src/
├── controllers/ # Parses req, validates, delegates, formats res
│ └── user.controller.js
├── services/ # Domain business rules, transactions, orchestration
│ └── user.service.js
├── repositories/ # Direct DB operations (Prisma, Mongoose, SQL)
│ └── user.repository.js
├── routes/ # Express Router mapping paths to controllers
│ └── user.routes.js
└── errors/ # Custom operational error classes
└── AppError.js
1. Custom Error Definition
JavaScript
// errors/AppError.js
export class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
2. Repository Layer (Data Access Only)
JavaScript
// repositories/user.repository.js
const mockDb = new Map();
export const userRepository = {
async findByEmail(email) {
for (const user of mockDb.values()) {
if (user.email === email) return user;
}
return null;
},
async create(userData) {
const id = `usr_${Date.now()}`;
const record = { id, ...userData, createdAt: new Date() };
mockDb.set(id, record);
return record;
}
};
3. Service Layer (Business Logic Only)
JavaScript
// services/user.service.js
import { userRepository } from '../repositories/user.repository.js';
import { AppError } from '../errors/AppError.js';
export const userService = {
async registerUser({ email, name, password }) {
const existing = await userRepository.findByEmail(email);
if (existing) {
throw new AppError('A user with this email already exists', 409);
}
// Business rule: scrub or transform before persisting
const newUser = await userRepository.create({ email, name, password });
// Return sanitized domain model
const { password: _, ...sanitized } = newUser;
return sanitized;
}
};
4. Controller Layer (HTTP Protocol Adaptation)
JavaScript
// controllers/user.controller.js
import { userService } from '../services/user.service.js';
export const userController = {
async register(req, res, next) {
try {
const user = await userService.registerUser(req.body);
res.status(201).json({ status: 'success', data: { user } });
} catch (err) {
next(err); // Hands errors off to centralized error middleware
}
}
};
Data Validation & Sanitization with Zod
Never trust client payloads. Validate schema contracts before incoming data reaches services or database layers.
Bash
npm install zod
Validation Middleware Factory
JavaScript
// middleware/validate.js
import { ZodError } from 'zod';
export const validate = (schema) => async (req, res, next) => {
try {
// Validates and replaces req properties with cleaned, parsed data
const parsed = await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
req.body = parsed.body ?? req.body;
req.query = parsed.query ?? req.query;
req.params = parsed.params ?? req.params;
next();
} catch (err) {
if (err instanceof ZodError) {
return res.status(422).json({
status: 'fail',
errors: err.errors.map((e) => ({
field: e.path.join('.'),
message: e.message,
})),
});
}
next(err);
}
};
Defining Schemas and Binding Routes
JavaScript
// schemas/user.schema.js
import { z } from 'zod';
export const registerUserSchema = z.object({
body: z.object({
name: z.string().trim().min(2, 'Name must be at least 2 characters'),
email: z.string().trim().email('Invalid email address').toLowerCase(),
password: z.string().min(8, 'Password must be at least 8 characters'),
age: z.coerce.number().int().positive().optional(),
}),
});
// routes/user.routes.js
import { Router } from 'express';
import { userController } from '../controllers/user.controller.js';
import { validate } from '../middleware/validate.js';
import { registerUserSchema } from '../schemas/user.schema.js';
const router = Router();
router.post('/register', validate(registerUserSchema), userController.register);
export default router;
File Handling: Uploads with Multer
Browsers and clients upload files via multipart/form-data. Because express.json() handles only JSON, processing multi-part binary streams requires dedicated middleware like multer.
Bash
npm install multer
Storage Engines: Disk vs. Memory
memoryStorage(): Stores files as rawBufferinstances in RAM (req.file.buffer). Recommended when streaming directly to cloud object storage (AWS S3, Google Cloud Storage, Cloudinary). Large uploads can cause memory spikes.diskStorage(): Streams files directly to the local file system. Keeps memory consumption low, but requires managing temporary files and disk cleanups.
Production Multer Configuration
JavaScript
// config/multer.js
import multer from 'multer';
import path from 'node:path';
import crypto from 'node:crypto';
import { AppError } from '../errors/AppError.js';
// Configure storage destination and collision-resistant file names
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = crypto.randomBytes(16).toString('hex');
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${file.fieldname}-${Date.now()}-${uniqueSuffix}${ext}`);
},
});
// Strict MIME type validation
const fileFilter = (req, file, cb) => {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new AppError('Invalid file format. Only JPEG, PNG, and WebP are allowed.', 400), false);
}
};
export const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5 Megabytes maximum
files: 1, // Restrict to a single file per request
},
});
Route Implementation
JavaScript
// routes/upload.routes.js
import { Router } from 'express';
import { upload } from '../config/multer.js';
const router = Router();
// Single file upload on field 'avatar'
router.post('/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'Please provide a file' });
}
// req.file contains metadata
const { path: diskPath, filename, size, mimetype } = req.file;
res.status(200).json({
status: 'success',
file: { filename, path: diskPath, size, mimetype },
});
});
// Multiple files upload on field 'documents' (up to 5)
router.post('/documents', upload.array('documents', 5), (req, res) => {
// req.files is an array of uploaded file objects
res.status(200).json({
uploadedCount: req.files.length,
files: req.files.map((f) => f.filename),
});
});
export default router;
