Route Handling
Basic Routes
The simplest route definition includes an HTTP method, a path, and a handler function.
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});In this example, when a client sends a GET request to the root path (/), the server responds with “Hello World!”.
Dynamic Routes
Dynamic routes allow URLs to include variables that can be passed as parameters to the handler function.
app.get('/user/:id', (req, res) => {
const userId = req.params.id;
res.send(`User ID: ${userId}`);
});Here, :id is a dynamic parameter accessible via req.params.id.
Route Parameters
In addition to dynamic route parameters, you can define additional parameters in the route handler function.
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`Search query: ${query}`);
});In this example, the req.query object contains all key-value pairs from the query string.
Route Groups
Route groups allow you to share a common prefix across a set of routes, which is useful for handling similar resources.
app.use('/api', (req, res, next) => {
console.log('API route accessed');
next();
});
app.get('/api/users', (req, res) => {
res.send('List of users');
});
app.get('/api/posts', (req, res) => {
res.send('List of posts');
});Here, /api is a route group, and all routes starting with /api pass through this middleware.
Error Handling
Error-handling middleware is a critical part of the Express routing system, used to catch and handle uncaught errors.
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});This middleware should be placed after all other routes to ensure it catches any errors.
Route Hierarchy
Express supports route hierarchies, allowing one route handler to call another route.
const router = express.Router();
router.get('/user/:id', (req, res) => {
const userId = req.params.id;
res.send(`User ID: ${userId}`);
});
app.use('/api', router);Here, router is a sub-router that can be used independently of the main application.
Route Redirection
Routes can be configured to redirect to another URL.
app.get('/old-page', (req, res) => {
res.redirect(301, '/new-page');
});This redirects all requests to /old-page to /new-page with a 301 status code.
Route Restrictions
Middleware can restrict access to certain routes, such as based on user permissions.
function adminOnly(req, res, next) {
if (req.user.isAdmin) {
next();
} else {
res.status(403).send('Forbidden');
}
}
app.get('/admin', adminOnly, (req, res) => {
res.send('Admin panel');
});In this example, only admin users can access the /admin path.
Route Caching
For static content or frequently accessed resources, caching can significantly improve performance.
const cache = require('node-cache');
const dataCache = new cache({ stdTTL: 60 });
app.get('/data', (req, res) => {
const cachedData = dataCache.get('data');
if (cachedData) {
res.send(cachedData);
} else {
// Fetch data from database or other source
const data = fetchData();
dataCache.set('data', data);
res.send(data);
}
});Route Testing
Using testing frameworks (e.g., Mocha or Jest) and request libraries (e.g., supertest) makes it easy to test route behavior.
const request = require('supertest');
const app = require('./app');
describe('GET /', () => {
test('should respond with Hello World!', async () => {
const response = await request(app).get('/');
expect(response.text).toBe('Hello World!');
});
});Route Parameters and Dynamic Routes
In Express, dynamic routes allow you to define URL paths with parameters that are populated with specific values provided by the client at runtime. Dynamic routes are powerful for building RESTful APIs and handling various URL structures.
Dynamic Route Basics
The basic form of a dynamic route uses a colon (:) to mark parameters in the route path. For example, you can define a route to fetch user profiles:
app.get('/users/:id', function(req, res) {
res.send('User ID is ' + req.params.id);
});In this example, :id is a dynamic parameter, and a client can access this route by providing a specific ID in the URL. For instance, a request to http://localhost:3000/users/123 returns "User ID is 123".
Accessing Route Parameters
Dynamic route parameters are accessible via the req.params object, where the keys are the parameter names and the values are the specific values provided by the client.
app.get('/users/:id', function(req, res) {
const userId = req.params.id;
// Query database or other data store to fetch user info
getUserInfo(userId, function(userInfo) {
res.json(userInfo);
});
});Multiple Dynamic Parameters
You can define multiple dynamic parameters in the same route path:
app.get('/users/:id/posts/:postId', function(req, res) {
const userId = req.params.id;
const postId = req.params.postId;
// Fetch a specific post for a specific user from the database
getPostById(userId, postId, function(post) {
res.json(post);
});
});Validating Route Parameters
To ensure client-provided parameters meet expectations, you can use middleware to validate them. For example, you can create middleware to check if :id is a valid integer:
function validateUserId(req, res, next) {
const userId = req.params.id;
if (!/^\d+$/.test(userId)) {
res.status(400).send('Invalid user ID');
return;
}
next();
}
app.get('/users/:id', validateUserId, function(req, res) {
const userId = req.params.id;
res.send('Valid user ID: ' + userId);
});Default Values for Route Parameters
To provide default values for dynamic parameters, you can use regular expressions and callback functions:
app.get('/users/:id([0-9]+)?', function(req, res) {
const userId = req.params.id || 'default';
res.send('User ID is ' + userId);
});In this example, if the client does not provide the :id parameter, it defaults to 'default'.
Optional Route Parameters
To make parameters optional, use regular expressions:
app.get('/users/:id?', function(req, res) {
const userId = req.params.id || 'no-id';
res.send('User ID is ' + userId);
});Order of Route Parameters
When defining multiple dynamic parameters, their order matters, as Express populates the req.params object based on the order of appearance:
app.get('/users/:id/posts/:postId', function(req, res) {
console.log(req.params); // { id: '123', postId: '456' }
});Route Parameter Namespaces
To avoid naming conflicts, use distinct namespaces for parameters, such as :userId and :postId instead of using :id for both.
Route Parameters and RESTful Design
Dynamic route parameters are central to RESTful API design, enabling clear and semantic URLs that clients can intuitively understand.
Route Grouping and Named Routes
Route Grouping
Route grouping allows you to apply a common prefix to a set of routes, reducing repetitive path prefixes and aiding in modularization and code organization.
Example Code
Suppose your application has multiple user-related API endpoints. You can use route grouping to organize these endpoints:
const express = require('express');
const app = express();
const userRouter = express.Router();
// User-related routes
userRouter.get('/', (req, res) => {
res.send('List all users');
});
userRouter.get('/:id', (req, res) => {
res.send(`Get user with ID ${req.params.id}`);
});
userRouter.post('/', (req, res) => {
res.send('Create a new user');
});
// Apply route group
app.use('/api/users', userRouter);
app.listen(3000, () => {
console.log('Server listening on port 3000');
});In this example, all requests starting with /api/users are handled by userRouter, allowing you to define user-related routes without repeating /api/users.
Named Routes
Named routes allow you to assign readable names to routes, which is useful for debugging and maintaining code. While Express does not natively support named routes, you can achieve similar functionality with some techniques.
Implementing Named Routes
You can create a route mapping object to implement named routes, allowing you to reference and manipulate routes by name.
const express = require('express');
const app = express();
const routes = {};
// Define named routes
routes.usersList = app.get('/api/users', (req, res) => {
res.send('List all users');
});
routes.userDetails = app.get('/api/users/:id', (req, res) => {
res.send(`Get user with ID ${req.params.id}`);
});
// Use named routes
console.log(routes.usersList.path); // Outputs: /api/users
console.log(routes.userDetails.path); // Outputs: /api/users/:id
app.listen(3000, () => {
console.log('Server listening on port 3000');
});Combining Route Grouping and Named Routes
Combining route grouping with named routes makes code cleaner and easier to maintain. Here’s an example:
const express = require('express');
const app = express();
const userRouter = express.Router();
const routes = {};
// Define named routes within a route group
routes.usersList = userRouter.get('/', (req, res) => {
res.send('List all users');
});
routes.userDetails = userRouter.get('/:id', (req, res) => {
res.send(`Get user with ID ${req.params.id}`);
});
// Apply route group
app.use('/api/users', userRouter);
app.listen(3000, () => {
console.log('Server listening on port 3000');
});Using Named Routes for Testing
Named routes are particularly useful during testing, as they allow you to reference routes by name instead of hardcoding paths, improving test code readability and maintainability.
const request = require('supertest');
const app = require('./app');
describe('User Routes', () => {
it('should list all users', (done) => {
request(app)
.get(routes.usersList.path)
.expect(200, 'List all users', done);
});
it('should get user details', (done) => {
request(app)
.get(routes.userDetails.path.replace(':id', '1'))
.expect(200, 'Get user with ID 1', done);
});
});Using Regular Expressions for Route Matching
Regular Expression Basics
Before diving in, let’s review key regular expression concepts:
- Character Classes:
[abc]matchesa,b, orc. - Ranges:
[a-z]matches lowercase letters fromatoz. - Any Character:
.matches any single character. - Repetition:
*(zero or more),+(one or more),?(zero or one). - Grouping:
()groups patterns, applying repetition or options to the group. - Non-Capturing Groups:
(?:)groups without capturing the match. - Lookahead:
(?=...)requires the pattern to be followed by..., without capturing.... - Negative Lookbehind:
(?<!...)requires the pattern not to be preceded by....
Using Regular Expressions for Route Matching
In Express, you can use regular expressions directly in route paths to define matching rules. Regular expressions should be enclosed in parentheses () and can include named capture groups for accessing matched parameters in req.params.
Example Code
Suppose you want a route that only accepts numeric parameters:
const express = require('express');
const app = express();
// Match numeric parameters with a regular expression
app.get('/user/:id([0-9]+)', (req, res) => {
const userId = req.params.id;
res.send(`User ID: ${userId}`);
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});In this example, ([0-9]+) is a regular expression that matches one or more digits. A request to /user/123 sets req.params.id to 123.
Complex Regular Expression Matching
Regular expressions can handle more complex route matching logic. For example, you can create a route that only accepts alphanumeric usernames:
app.get('/user/:username([a-zA-Z0-9]+)', (req, res) => {
const username = req.params.username;
res.send(`Username: ${username}`);
});Or a route that only accepts email addresses starting with @:
app.get('/email/@([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', (req, res) => {
const email = req.params['0'];
res.send(`Email: ${email}`);
});Optional Parameters
Regular expressions can define optional route parameters. For example, the following route matches /product/123 and /product/123/review:
app.get('/product/:id([0-9]+)/review?', (req, res) => {
const productId = req.params.id;
const hasReview = req.params.review !== undefined;
res.send(`Product ID: ${productId}, Has Review: ${hasReview}`);
});Multi-Condition Matching
Use logical operators (e.g., |) in regular expressions to match multiple conditions. For example, the following route matches /user/123 or /user/john:
app.get('/user/:id([0-9]+|john)', (req, res) => {
const userId = req.params.id;
res.send(`User ID: ${userId}`);
});Route Controllers
What Are Route Controllers?
A route controller is a design pattern where each route handler function is encapsulated in a separate controller function. This centralizes the business logic for each route, making the code clearer and easier to maintain and extend.
Creating Controller Files
First, create a controller file for each route or group of related routes. For example, for user-related requests, create a usersController.js file:
// controllers/usersController.js
module.exports = {
getUsers: (req, res) => {
// Fetch user list from database
const users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
];
res.json(users);
},
getUserById: (req, res) => {
const userId = req.params.id;
// Fetch user by ID from database
const user = { id: userId, name: `User ${userId}` };
res.json(user);
},
createUser: (req, res) => {
const newUser = req.body;
// Save new user to database
res.status(201).json(newUser);
}
};Using Controllers in Routes
In your main application file, import the controller and associate it with the corresponding routes:
// app.js
const express = require('express');
const app = express();
const usersController = require('./controllers/usersController');
app.get('/users', usersController.getUsers);
app.get('/users/:id', usersController.getUserById);
app.post('/users', usersController.createUser);
app.listen(3000, () => {
console.log('Server listening on port 3000');
});Advanced Controller Usage
Middleware Integration: You can use middleware in controller functions, such as for authentication or logging.
const authenticate = (req, res, next) => {
// Authentication logic
next();
};
app.get('/users/:id', authenticate, usersController.getUserById);Error Handling: Controller functions can throw errors, which are caught by Express’s error-handling middleware.
usersController.getUserById = (req, res, next) => {
const userId = req.params.id;
if (!userId) {
return next(new Error('User ID is required'));
}
// Other logic...
};Controller Interaction with Models
In real applications, controllers typically interact with a model layer to handle data storage and retrieval. For example, replace hardcoded data with database operations:
// controllers/usersController.js
const User = require('../models/User');
module.exports = {
getUsers: async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
res.status(500).json({ message: error.message });
}
},
// Other controller methods...
};Controllers and Service Layer
In complex projects, you may introduce a service layer to further abstract business logic, decoupling it from the data access layer.
// services/userService.js
const User = require('../models/User');
module.exports = {
getUsers: async () => {
return await User.find();
}
};
// controllers/usersController.js
const userService = require('../services/userService');
module.exports = {
getUsers: async (req, res) => {
try {
const users = await userService.getUsers();
res.json(users);
} catch (error) {
res.status(500).json({ message: error.message });
}
}
};Testing Controllers
Since controller functions are typically independent, they are well-suited for unit testing. Use testing frameworks like Mocha or Jest to write test cases.
// tests/usersController.test.js
const usersController = require('../controllers/usersController');
const userService = require('../services/userService');
describe('Users Controller', () => {
beforeEach(() => {
jest.spyOn(userService, 'getUsers').mockResolvedValue([{ id: 1, name: 'Test User' }]);
});
afterEach(() => {
userService.getUsers.mockRestore();
});
it('should return a list of users', async () => {
const req = {};
const res = {
json: jest.fn()
};
await usersController.getUsers(req, res);
expect(res.json).toHaveBeenCalledWith([{ id: 1, name: 'Test User' }]);
});
});Project Architecture
File and Directory Structure
A good starting point is designing a logical file and directory structure. Below is a typical directory structure for an Express application:
my-app/
|-- server.js
|-- package.json
|-- controllers/
| |-- index.js
| |-- usersController.js
|-- models/
| |-- index.js
| |-- userModel.js
|-- routers/
| |-- index.js
| |-- usersRouter.js
|-- middleware/
| |-- index.js
| |-- authMiddleware.js
|-- views/
| |-- layout.ejs
| |-- index.ejs
|-- public/
| |-- css/
| |-- js/
|-- test/
| |-- index.jsserver.js: Main application entry point, responsible for starting the server and loading configurations.controllers/: Directory for route controllers.models/: Directory for data models, typically interacting with a database.routers/: Directory for route modules.middleware/: Directory for middleware.views/: Directory for view templates.public/: Directory for static files.test/: Directory for test files.
Separating Routes and Controllers
Separating routes and controllers is a key step in modularization. Each controller file should focus on specific business logic, while route files map requests to the appropriate controller functions.
routers/usersRouter.js
const express = require('express');
const router = express.Router();
const usersController = require('../controllers/usersController');
router.get('/', usersController.getUsers);
router.get('/:id', usersController.getUserById);
router.post('/', usersController.createUser);
module.exports = router;controllers/usersController.js
module.exports = {
getUsers: (req, res) => {
// Logic handling
},
getUserById: (req, res) => {
// Logic handling
},
createUser: (req, res) => {
// Logic handling
}
};Data Models and Service Layer
Data models and related service layers should be separated from controllers to improve modularization and code reuse.
models/userModel.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
name: String,
email: String
});
module.exports = mongoose.model('User', UserSchema);services/userService.js
const User = require('../models/userModel');
module.exports = {
getUsers: async () => {
return await User.find();
},
getUserById: async (id) => {
return await User.findById(id);
},
createUser: async (user) => {
const newUser = new User(user);
return await newUser.save();
}
};Modularizing Middleware
Middleware can also be modularized, especially for reusable functionality like logging or authentication.
middleware/authMiddleware.js
module.exports = (req, res, next) => {
// Authentication logic
next();
};Main Application Entry Point
In the main application entry file, import and use the above modules.
server.js
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const usersRouter = require('./routers/usersRouter');
const authMiddleware = require('./middleware/authMiddleware');
app.use(bodyParser.json());
app.use(authMiddleware);
app.use('/api/users', usersRouter);
app.listen(3000, () => {
console.log('Server listening on port 3000');
});



