Express.js in Practice
What is Express.js?
Express.js is the most widely used web framework for Node.js. It abstracts the complex parts of the http module and lets you write routing, middleware, and error handling concisely.
Since its release in 2010, it has remained the standard of the Node.js ecosystem for over 15 years. With over 30 million weekly downloads, it still dominates comparisons against newer alternatives like Fastify, Koa, and Hono.
Installation and Basic Structure
mkdir express-app && cd express-app
npm init -y
npm install express
npm install --save-dev nodemon
// app.js
const express = require('express');
const app = express();
// Register middleware
app.use(express.json()); // JSON body parsing
app.use(express.urlencoded({ extended: true })); // HTML form parsing
// Define route
app.get('/', (req, res) => {
res.json({ message: 'Express server is running!' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running: http://localhost:${PORT}`);
});
module.exports = app; // export for testing
// package.json scripts
{
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
}
Router
Basic Route Methods
const express = require('express');
const app = express();
app.use(express.json());
// Routes by HTTP method
app.get('/users', (req, res) => {
res.json({ users: [] });
});
app.post('/users', (req, res) => {
const user = req.body;
res.status(201).json({ created: user });
});
app.put('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ updated: id, data: req.body });
});
app.patch('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ patched: id });
});
app.delete('/users/:id', (req, res) => {
const { id } = req.params;
res.status(204).send(); // no content
});
// Handle all HTTP methods
app.all('/any', (req, res) => {
res.json({ method: req.method });
});
Modularizing with Router()
// routes/users.js
const express = require('express');
const router = express.Router();
// Fake database
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
let nextId = 3;
// GET /users
router.get('/', (req, res) => {
const { page = 1, limit = 10, search } = req.query;
let result = users;
if (search) {
result = users.filter(u =>
u.name.includes(search) || u.email.includes(search)
);
}
const start = (page - 1) * limit;
const paged = result.slice(start, start + Number(limit));
res.json({
total: result.length,
page: Number(page),
limit: Number(limit),
data: paged,
});
});
// GET /users/:id
router.get('/:id', (req, res) => {
const id = Number(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// POST /users
router.post('/', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'name and email are required' });
}
const user = { id: nextId++, name, email };
users.push(user);
res.status(201).json(user);
});
// PUT /users/:id
router.put('/:id', (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users[index] = { id, ...req.body };
res.json(users[index]);
});
// DELETE /users/:id
router.delete('/:id', (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
});
module.exports = router;
// Connect router in app.js
const usersRouter = require('./routes/users');
app.use('/users', usersRouter);
// /users → router.get('/')
// /users/1 → router.get('/:id')
Middleware
Middleware is a function with the (req, res, next) signature. It is the core of Express.
Global Middleware
const express = require('express');
const app = express();
// Request logger (global)
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
});
next(); // must call next() to proceed
});
// Authentication middleware (global)
app.use((req, res, next) => {
req.user = null; // default
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.slice(7);
// In practice, verify JWT here
if (token === 'valid-token') {
req.user = { id: 1, name: 'Admin' };
}
}
next();
});
Per-Route Middleware
// Apply to specific routes only
function requireAuth(req, res, next) {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
}
function requireAdmin(req, res, next) {
if (req.user?.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
// Multiple middleware on a single route
app.get('/profile', requireAuth, (req, res) => {
res.json(req.user);
});
// Admin-only route
app.delete('/users/:id', requireAuth, requireAdmin, (req, res) => {
res.json({ deleted: req.params.id });
});
// Apply middleware to a Router
const adminRouter = express.Router();
adminRouter.use(requireAuth, requireAdmin); // applies to all routes in this router
adminRouter.get('/stats', (req, res) => res.json({ stats: {} }));
app.use('/admin', adminRouter);
Error Handler Middleware
Error handlers have 4 parameters (err, req, res, next). They must be registered last.
// Custom error class
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.name = 'AppError';
}
}
// Async wrapper (passes errors from async functions to next)
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Throw error from a route
app.get('/error-test', asyncHandler(async (req, res) => {
throw new AppError('Test error', 400);
}));
// 404 handler (after all routes)
app.use((req, res, next) => {
next(new AppError(`${req.url} not found`, 404));
});
// Global error handler (4 parameters!)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Server error occurred';
// Only expose stack trace in development
const response = {
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
};
console.error(`[ERROR] ${statusCode}: ${message}`);
res.status(statusCode).json(response);
});
Request Object (req)
app.get('/users/:id/posts/:postId', (req, res) => {
// Route parameters
console.log(req.params); // { id: '1', postId: '42' }
// Query string
// URL: /users/1/posts/42?sort=date&order=desc&tags[]=js&tags[]=node
console.log(req.query); // { sort: 'date', order: 'desc', tags: ['js', 'node'] }
// Request body (requires express.json() middleware)
console.log(req.body); // { title: 'Title', content: 'Content' }
// Headers
console.log(req.headers);
console.log(req.get('Authorization')); // get header value
// Other useful properties
console.log(req.method); // GET
console.log(req.path); // /users/1/posts/42
console.log(req.hostname); // localhost
console.log(req.ip); // 127.0.0.1
console.log(req.protocol); // http or https
console.log(req.secure); // true if https
console.log(req.xhr); // true if XMLHttpRequest
res.json({ params: req.params });
});
Response Object (res)
app.get('/response-demo', (req, res) => {
// JSON response (Content-Type: application/json set automatically)
res.json({ message: 'success' });
// Status code chaining
res.status(201).json({ created: true });
// String response
res.send('text response');
// HTML response
res.send('<h1>HTML response</h1>');
// File download
res.download('./file.pdf', 'download.pdf');
// Send file (displays in browser)
res.sendFile('/absolute/path/to/file.html');
// Redirect
res.redirect('/new-url');
res.redirect(301, '/permanent-url'); // permanent redirect
// Set headers
res.set('X-Custom', 'value');
res.set({
'X-Custom-1': 'value1',
'X-Custom-2': 'value2',
});
// Status code only
res.sendStatus(404); // includes "Not Found" text
res.status(204).end(); // empty response
});
CORS Configuration
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Allow all origins (development)
app.use(cors());
// Allow specific origins only (production)
const corsOptions = {
origin: [
'https://myapp.com',
'https://www.myapp.com',
'http://localhost:3000', // development environment
],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // allow requests with cookies
maxAge: 86400, // preflight cache duration (seconds)
};
app.use(cors(corsOptions));
// Apply to a specific route only
app.get('/public', cors(), (req, res) => {
res.json({ public: true });
});
Serving Static Files
const path = require('path');
// Serve files from /public folder as static
// http://localhost:3000/images/logo.png → public/images/logo.png
app.use(express.static(path.join(__dirname, 'public')));
// Virtual path prefix
// http://localhost:3000/static/style.css → public/style.css
app.use('/static', express.static(path.join(__dirname, 'public')));
// Cache settings
app.use(express.static('public', {
maxAge: '1d', // 1-day cache
etag: true,
lastModified: true,
index: 'index.html', // default file for directories
}));
Practical: RESTful API Server
// server.js — complete RESTful API example
const express = require('express');
const cors = require('cors');
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Request logger
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next();
});
// Fake DB
const db = {
users: new Map([
[1, { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' }],
[2, { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' }],
]),
nextId: 3,
};
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
// Users CRUD
app.get('/api/users', (req, res) => {
const users = Array.from(db.users.values());
res.json({ count: users.length, data: users });
});
app.get('/api/users/:id', (req, res) => {
const user = db.users.get(Number(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'name and email are required' });
}
const user = { id: db.nextId++, name, email, role: 'user' };
db.users.set(user.id, user);
res.status(201).json(user);
});
app.put('/api/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!db.users.has(id)) return res.status(404).json({ error: 'User not found' });
const updated = { ...db.users.get(id), ...req.body, id };
db.users.set(id, updated);
res.json(updated);
});
app.delete('/api/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!db.users.has(id)) return res.status(404).json({ error: 'User not found' });
db.users.delete(id);
res.status(204).send();
});
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: `${req.method} ${req.url} not found` });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(err.statusCode || 500).json({ error: err.message });
});
app.listen(3000, () => {
console.log('RESTful API server: http://localhost:3000');
console.log('API routes:');
console.log(' GET /api/users');
console.log(' GET /api/users/:id');
console.log(' POST /api/users');
console.log(' PUT /api/users/:id');
console.log(' DELETE /api/users/:id');
});
Pro Tips
Request Validation (express-validator)
npm install express-validator
const { body, param, validationResult } = require('express-validator');
function validate(validations) {
return async (req, res, next) => {
await Promise.all(validations.map(v => v.run(req)));
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
}
app.post('/users',
validate([
body('name').trim().notEmpty().withMessage('Name is required'),
body('email').isEmail().withMessage('Invalid email format'),
body('age').optional().isInt({ min: 1, max: 150 }).withMessage('Age must be between 1 and 150'),
]),
(req, res) => {
res.status(201).json({ created: req.body });
}
);
Rate Limiting
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests
message: { error: 'Too many requests, please retry after 15 minutes' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api', limiter); // apply to API routes only