Routing and Navigation
Overview
Routing is the mechanism that determines how an application responds to client requests for specific endpoints. This chapter covers HTTP routing in Node.js, from basic routing with the built-in http module to advanced routing with Express.js.
Basic HTTP Routing
Manual Routing with HTTP Module
javascript
// basic-routing.js
const http = require('http');
const url = require('url');
const querystring = require('querystring');
class BasicRouter {
constructor() {
this.routes = new Map();
}
addRoute(method, path, handler) {
const key = `${method.toUpperCase()}:${path}`;
this.routes.set(key, handler);
}
get(path, handler) {
this.addRoute('GET', path, handler);
}
post(path, handler) {
this.addRoute('POST', path, handler);
}
put(path, handler) {
this.addRoute('PUT', path, handler);
}
delete(path, handler) {
this.addRoute('DELETE', path, handler);
}
async handleRequest(req, res) {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const method = req.method;
const query = parsedUrl.query;
// Add query parameters to request
req.query = query;
// Parse body for POST/PUT requests
if (method === 'POST' || method === 'PUT') {
req.body = await this.parseBody(req);
}
// Find matching route
const routeKey = `${method}:${path}`;
const handler = this.routes.get(routeKey);
if (handler) {
try {
await handler(req, res);
} catch (error) {
this.handleError(res, error);
}
} else {
this.handle404(res);
}
}
async parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const contentType = req.headers['content-type'];
if (contentType && contentType.includes('application/json')) {
resolve(JSON.parse(body));
} else if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
resolve(querystring.parse(body));
} else {
resolve(body);
}
} catch (error) {
reject(error);
}
});
req.on('error', reject);
});
}
handleError(res, error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Internal Server Error',
message: error.message
}));
}
handle404(res) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Not Found',
message: 'Route not found'
}));
}
}
// Usage example
const router = new BasicRouter();
// Define routes
router.get('/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Welcome to the API' }));
});
router.get('/users', (req, res) => {
const { page = 1, limit = 10 } = req.query;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
users: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
],
pagination: { page: parseInt(page), limit: parseInt(limit) }
}));
});
router.post('/users', (req, res) => {
const userData = req.body;
// Simulate user creation
const newUser = {
id: Date.now(),
...userData,
createdAt: new Date().toISOString()
};
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(newUser));
});
// Create server
const server = http.createServer((req, res) => {
router.handleRequest(req, res);
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});Express.js Routing
Basic Express Routing
javascript
// express-basic-routing.js
const express = require('express');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Basic routes
app.get('/', (req, res) => {
res.json({ message: 'Welcome to Express API' });
});
// Route with parameters
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.json({
user: {
id: parseInt(id),
name: `User ${id}`,
email: `user${id}@example.com`
}
});
});
// Route with query parameters
app.get('/search', (req, res) => {
const { q, category, page = 1, limit = 10 } = req.query;
res.json({
query: q,
category,
results: [],
pagination: {
page: parseInt(page),
limit: parseInt(limit)
}
});
});
// Multiple route parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({
post: {
id: parseInt(postId),
userId: parseInt(userId),
title: `Post ${postId} by User ${userId}`
}
});
});
// Route with optional parameters
app.get('/products/:category/:subcategory?', (req, res) => {
const { category, subcategory } = req.params;
res.json({
category,
subcategory: subcategory || 'all',
products: []
});
});
// Wildcard routes
app.get('/files/*', (req, res) => {
const filePath = req.params[0];
res.json({
message: `Accessing file: ${filePath}`,
fullPath: `/files/${filePath}`
});
});
app.listen(3000, () => {
console.log('Express server running on http://localhost:3000');
});Advanced Express Routing
javascript
// express-advanced-routing.js
const express = require('express');
const app = express();
app.use(express.json());
// Route-specific middleware
const authenticateUser = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
// Simulate token validation
req.user = { id: 1, name: 'John Doe' };
next();
};
const validateUserData = (req, res, next) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
next();
};
// Routes with middleware
app.get('/protected', authenticateUser, (req, res) => {
res.json({
message: 'This is a protected route',
user: req.user
});
});
app.post('/users', validateUserData, (req, res) => {
const userData = req.body;
res.status(201).json({
message: 'User created successfully',
user: {
id: Date.now(),
...userData,
createdAt: new Date().toISOString()
}
});
});
// Route handlers with multiple functions
app.get('/multi-handler',
(req, res, next) => {
console.log('First handler');
req.customData = 'Hello from first handler';
next();
},
(req, res, next) => {
console.log('Second handler');
req.customData += ' and second handler';
next();
},
(req, res) => {
res.json({ message: req.customData });
}
);
// Route parameter validation
app.param('userId', (req, res, next, userId) => {
const id = parseInt(userId);
if (isNaN(id) || id <= 0) {
return res.status(400).json({ error: 'Invalid user ID' });
}
req.userId = id;
next();
});
app.get('/users/:userId', (req, res) => {
res.json({
user: {
id: req.userId,
name: `User ${req.userId}`
}
});
});
// Error handling for specific routes
app.get('/error-demo', (req, res, next) => {
const error = new Error('Something went wrong');
error.status = 500;
next(error);
});
// Route-level error handler
app.use('/api', (error, req, res, next) => {
res.status(error.status || 500).json({
error: error.message,
path: req.path
});
});
app.listen(3000);Router Module
Creating Modular Routes
javascript
// routes/users.js
const express = require('express');
const router = express.Router();
// Middleware specific to this router
router.use((req, res, next) => {
console.log('Users router middleware');
req.timestamp = new Date().toISOString();
next();
});
// Mock data
let users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
// GET /users
router.get('/', (req, res) => {
const { page = 1, limit = 10, search } = req.query;
let filteredUsers = users;
if (search) {
filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase()) ||
user.email.toLowerCase().includes(search.toLowerCase())
);
}
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
res.json({
users: paginatedUsers,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredUsers.length,
pages: Math.ceil(filteredUsers.length / limit)
},
timestamp: req.timestamp
});
});
// GET /users/:id
router.get('/:id', (req, res) => {
const id = parseInt(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, timestamp: req.timestamp });
});
// 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 newUser = {
id: Math.max(...users.map(u => u.id)) + 1,
name,
email,
createdAt: req.timestamp
};
users.push(newUser);
res.status(201).json({
message: 'User created successfully',
user: newUser
});
});
// PUT /users/:id
router.put('/:id', (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
const { name, email } = req.body;
users[userIndex] = {
...users[userIndex],
name: name || users[userIndex].name,
email: email || users[userIndex].email,
updatedAt: req.timestamp
};
res.json({
message: 'User updated successfully',
user: users[userIndex]
});
});
// DELETE /users/:id
router.delete('/:id', (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
const deletedUser = users.splice(userIndex, 1)[0];
res.json({
message: 'User deleted successfully',
user: deletedUser
});
});
module.exports = router;javascript
// routes/posts.js
const express = require('express');
const router = express.Router();
let posts = [
{ id: 1, title: 'First Post', content: 'This is the first post', userId: 1 },
{ id: 2, title: 'Second Post', content: 'This is the second post', userId: 2 }
];
// GET /posts
router.get('/', (req, res) => {
const { userId, page = 1, limit = 10 } = req.query;
let filteredPosts = posts;
if (userId) {
filteredPosts = posts.filter(post => post.userId === parseInt(userId));
}
res.json({
posts: filteredPosts,
total: filteredPosts.length
});
});
// GET /posts/:id
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
const post = posts.find(p => p.id === id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ post });
});
// POST /posts
router.post('/', (req, res) => {
const { title, content, userId } = req.body;
if (!title || !content || !userId) {
return res.status(400).json({ error: 'Title, content, and userId are required' });
}
const newPost = {
id: Math.max(...posts.map(p => p.id)) + 1,
title,
content,
userId: parseInt(userId),
createdAt: new Date().toISOString()
};
posts.push(newPost);
res.status(201).json({
message: 'Post created successfully',
post: newPost
});
});
module.exports = router;javascript
// app.js - Using modular routes
const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');
const app = express();
// Global middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
next();
});
// Mount routers
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
// Root route
app.get('/', (req, res) => {
res.json({
message: 'API Server',
endpoints: {
users: '/api/users',
posts: '/api/posts'
}
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
error: 'Route not found',
path: req.originalUrl
});
});
// Error handler
app.use((error, req, res, next) => {
console.error('Error:', error);
res.status(error.status || 500).json({
error: error.message || 'Internal Server Error',
path: req.path
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});Advanced Routing Patterns
Route Versioning
javascript
// routes/v1/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({
version: 'v1',
users: [
{ id: 1, name: 'John Doe' }
]
});
});
module.exports = router;javascript
// routes/v2/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({
version: 'v2',
data: {
users: [
{
id: 1,
firstName: 'John',
lastName: 'Doe',
profile: {
email: 'john@example.com'
}
}
],
meta: {
total: 1,
page: 1
}
}
});
});
module.exports = router;javascript
// app-versioned.js
const express = require('express');
const app = express();
app.use(express.json());
// Version 1 routes
app.use('/api/v1/users', require('./routes/v1/users'));
// Version 2 routes
app.use('/api/v2/users', require('./routes/v2/users'));
// Default to latest version
app.use('/api/users', require('./routes/v2/users'));
// Version detection middleware
app.use('/api', (req, res, next) => {
const version = req.headers['api-version'] || 'v2';
req.apiVersion = version;
next();
});
app.listen(3000);Dynamic Route Loading
javascript
// utils/route-loader.js
const fs = require('fs');
const path = require('path');
class RouteLoader {
constructor(app) {
this.app = app;
this.routes = new Map();
}
loadRoutes(routesDir) {
const routeFiles = this.getRouteFiles(routesDir);
for (const file of routeFiles) {
this.loadRoute(file);
}
}
getRouteFiles(dir) {
const files = [];
const readDir = (currentDir) => {
const items = fs.readdirSync(currentDir);
for (const item of items) {
const fullPath = path.join(currentDir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
readDir(fullPath);
} else if (item.endsWith('.js') && !item.startsWith('_')) {
files.push(fullPath);
}
}
};
readDir(dir);
return files;
}
loadRoute(filePath) {
try {
const routeModule = require(filePath);
const routePath = this.getRoutePathFromFile(filePath);
if (typeof routeModule === 'function') {
// Route module exports a function that returns a router
const router = routeModule();
this.app.use(routePath, router);
} else if (routeModule.router) {
// Route module exports an object with router property
this.app.use(routePath, routeModule.router);
} else {
// Route module exports a router directly
this.app.use(routePath, routeModule);
}
this.routes.set(routePath, filePath);
console.log(`Loaded route: ${routePath} from ${filePath}`);
} catch (error) {
console.error(`Failed to load route from ${filePath}:`, error.message);
}
}
getRoutePathFromFile(filePath) {
const relativePath = path.relative(path.join(__dirname, '../routes'), filePath);
const routePath = '/' + relativePath
.replace(/\\/g, '/') // Convert Windows paths
.replace(/\.js$/, '') // Remove .js extension
.replace(/\/index$/, '') // Remove /index
.replace(/\[([^\]]+)\]/g, ':$1'); // Convert [param] to :param
return routePath === '/' ? '' : routePath;
}
reloadRoute(routePath) {
const filePath = this.routes.get(routePath);
if (filePath) {
// Clear require cache
delete require.cache[require.resolve(filePath)];
// Reload the route
this.loadRoute(filePath);
}
}
getLoadedRoutes() {
return Array.from(this.routes.keys());
}
}
module.exports = RouteLoader;Route Guards and Middleware
javascript
// middleware/auth-guards.js
const jwt = require('jsonwebtoken');
const config = require('../config');
// Authentication guard
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, config.jwt.secret);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Authorization guard
const authorize = (roles = []) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (roles.length && !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Rate limiting guard
const rateLimit = (maxRequests = 100, windowMs = 15 * 60 * 1000) => {
const requests = new Map();
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
const windowStart = now - windowMs;
// Clean old entries
const userRequests = requests.get(key) || [];
const validRequests = userRequests.filter(time => time > windowStart);
if (validRequests.length >= maxRequests) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil((validRequests[0] + windowMs - now) / 1000)
});
}
validRequests.push(now);
requests.set(key, validRequests);
next();
};
};
// Validation guard
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(detail => detail.message)
});
}
next();
};
};
module.exports = {
authenticate,
authorize,
rateLimit,
validate
};javascript
// routes/protected.js - Using guards
const express = require('express');
const { authenticate, authorize, rateLimit } = require('../middleware/auth-guards');
const router = express.Router();
// Apply rate limiting to all routes in this router
router.use(rateLimit(50, 15 * 60 * 1000)); // 50 requests per 15 minutes
// Public route
router.get('/public', (req, res) => {
res.json({ message: 'This is a public route' });
});
// Authenticated route
router.get('/private', authenticate, (req, res) => {
res.json({
message: 'This is a private route',
user: req.user
});
});
// Admin only route
router.get('/admin', authenticate, authorize(['admin']), (req, res) => {
res.json({
message: 'This is an admin-only route',
user: req.user
});
});
// Multiple roles
router.get('/moderator', authenticate, authorize(['admin', 'moderator']), (req, res) => {
res.json({
message: 'This route is for admins and moderators',
user: req.user
});
});
module.exports = router;Next Steps
In the next chapter, we'll explore state management in Node.js applications, including session management, caching strategies, and data persistence.
Practice Exercises
- Create a RESTful API with full CRUD operations using Express routers
- Implement API versioning with backward compatibility
- Build a route guard system with role-based access control
- Create a dynamic route loading system that supports hot reloading
Key Takeaways
- Routing determines how applications respond to client requests
- Express.js provides powerful routing capabilities with middleware support
- Modular routing improves code organization and maintainability
- Route guards enable authentication and authorization
- API versioning allows for backward compatibility
- Dynamic route loading enables flexible application architecture
- Proper error handling in routes improves user experience