Express Route Handlers
Introduction
Route handlers are the functions that execute when a specific route is matched in an Express application. They represent the core of your application's logic, determining how to respond to client requests. Whether you're building a simple website or a complex API, understanding route handlers is essential for creating effective Express applications.
In this guide, we'll explore how route handlers work, how to define them, and best practices for organizing your application logic.
What Is a Route Handler?
A route handler is a function that receives HTTP requests and returns responses. In Express, these functions follow a specific signature:
function(req, res, next) {
// Your logic here
}
Where:
req
is the request object containing information about the HTTP requestres
is the response object used to send back data to the clientnext
is a function that passes control to the next matching route handler
Let's examine how these handlers work within the Express routing system.
Basic Route Handler Syntax
The most common way to define a route handler is directly in the route definition:
const express = require('express');
const app = express();
app.get('/hello', (req, res) => {
res.send('Hello, World!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
In this example, the function (req, res) => { res.send('Hello, World!'); }
is the route handler for GET requests to the /hello
path.
Response Methods
Route handlers use the response object (res
) to send data back to clients. Here are the most common response methods:
res.send()
Sends a response of various types:
app.get('/text', (req, res) => {
res.send('Plain text response');
});
app.get('/html', (req, res) => {
res.send('<h1>HTML response</h1>');
});
app.get('/json', (req, res) => {
res.send({ message: 'This is automatically converted to JSON' });
});
res.json()
Specifically for sending JSON responses:
app.get('/user', (req, res) => {
const user = {
id: 1,
name: 'John Doe',
email: '[email protected]'
};
res.json(user);
});
res.render()
Renders a view template (requires a template engine like EJS or Pug):
app.get('/profile', (req, res) => {
res.render('profile', {
username: 'JohnDoe',
isAdmin: true
});
});
res.status()
Sets the HTTP status code (chainable with other methods):
app.get('/not-found', (req, res) => {
res.status(404).send('Resource not found');
});
app.get('/created', (req, res) => {
res.status(201).json({ message: 'Resource created successfully' });
});
Request Data Access
Route handlers can access data from the request in several ways:
URL Parameters
Parameters defined in the route path with :paramName
syntax:
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.send(`Fetching post ${postId} by user ${userId}`);
});
Example request: GET /users/42/posts/123
Output: Fetching post 123 by user 42
Query Parameters
Data sent through the URL query string:
app.get('/search', (req, res) => {
const { q, limit = 10 } = req.query;
res.send(`Searching for "${q}" with limit ${limit}`);
});
Example request: GET /search?q=express&limit=20
Output: Searching for "express" with limit 20
Request Body
Data sent in the request body (requires appropriate middleware):
// Add middleware to parse JSON request bodies
app.use(express.json());
app.post('/api/users', (req, res) => {
const { username, email } = req.body;
// In a real app, you would validate and save to database
console.log(`Creating user: ${username} (${email})`);
res.status(201).json({
message: 'User created successfully',
user: { username, email, id: Date.now() }
});
});
Example request:
POST /api/users
Content-Type: application/json
{
"username": "new_user",
"email": "[email protected]"
}
Example response:
{
"message": "User created successfully",
"user": {
"username": "new_user",
"email": "[email protected]",
"id": 1624568735893
}
}
Named Route Handler Functions
For better code organization, you can define route handlers as named functions:
// Define handlers separately
function getAllUsers(req, res) {
res.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
}
function getUserById(req, res) {
const id = parseInt(req.params.id);
// In a real app, you would fetch from a database
res.json({ id, name: id === 1 ? 'Alice' : 'Bob' });
}
function createUser(req, res) {
const { name } = req.body;
res.status(201).json({ id: Date.now(), name });
}
// Use them in routes
app.get('/users', getAllUsers);
app.get('/users/:id', getUserById);
app.post('/users', createUser);
This approach makes your code more maintainable and easier to test.
Multiple Route Handlers
Express allows you to chain multiple handlers for a single route:
// Authentication middleware
function checkAuth(req, res, next) {
if (req.query.apiKey === 'secret123') {
next(); // Proceed to the next handler
} else {
res.status(401).send('Unauthorized');
}
}
// Rate limiting middleware
function rateLimit(req, res, next) {
// Simple example - in production use a proper rate limiter
console.log('Request passed rate limiting');
next();
}
// Main route handler
function getSecretData(req, res) {
res.json({ secret: 'Top secret information' });
}
// Chain all three handlers for this route
app.get('/api/secret', checkAuth, rateLimit, getSecretData);
This pattern is extremely powerful for separating concerns in your application.
Handler Organization in Real Projects
In larger applications, it's common to organize handlers into controller files:
// userController.js
exports.getAllUsers = (req, res) => {
// Logic to get users
};
exports.getUserById = (req, res) => {
// Logic to get specific user
};
exports.createUser = (req, res) => {
// Logic to create user
};
// In your main routes file:
const userController = require('./controllers/userController');
app.get('/users', userController.getAllUsers);
app.get('/users/:id', userController.getUserById);
app.post('/users', userController.createUser);
This approach follows the MVC (Model-View-Controller) pattern and keeps your code organized as it grows.
Error Handling in Route Handlers
Proper error handling is crucial in Express applications. You can handle errors in several ways:
Try-Catch in Async Handlers
For async route handlers, use try-catch:
app.get('/users/:id', async (req, res) => {
try {
const id = req.params.id;
// In a real app, this would be a database call
const user = await findUserById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error occurred' });
}
});
Using the next()
Function with Errors
You can pass errors to Express's error handling system:
app.get('/users/:id', async (req, res, next) => {
try {
const user = await findUserById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
next(error); // Pass error to Express error handler
}
});
// Global error handler (define at the end of your middleware stack)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Something went wrong!',
message: process.env.NODE_ENV === 'production' ? null : err.message
});
});
Real-World Example: Building a Simple Blog API
Let's put everything together to create a simple blog API:
const express = require('express');
const app = express();
// Middleware
app.use(express.json());
// In-memory data store (use a database in real applications)
const posts = [
{ id: 1, title: 'Express Basics', content: 'This is an introduction to Express', author: 'admin' },
{ id: 2, title: 'Route Handlers', content: 'Learn about Express route handlers', author: 'admin' }
];
// Route handlers for blog posts
const blogController = {
// Get all posts
getAllPosts: (req, res) => {
const { author } = req.query;
if (author) {
const filteredPosts = posts.filter(post => post.author === author);
return res.json(filteredPosts);
}
res.json(posts);
},
// Get post by ID
getPostById: (req, res) => {
const id = parseInt(req.params.id);
const post = posts.find(post => post.id === id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(post);
},
// Create a new post
createPost: (req, res) => {
const { title, content, author } = req.body;
// Validate required fields
if (!title || !content || !author) {
return res.status(400).json({ error: 'Missing required fields' });
}
const newPost = {
id: posts.length + 1,
title,
content,
author,
createdAt: new Date().toISOString()
};
posts.push(newPost);
res.status(201).json(newPost);
},
// Update a post
updatePost: (req, res) => {
const id = parseInt(req.params.id);
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex === -1) {
return res.status(404).json({ error: 'Post not found' });
}
const { title, content, author } = req.body;
const updatedPost = {
...posts[postIndex],
title: title || posts[postIndex].title,
content: content || posts[postIndex].content,
author: author || posts[postIndex].author,
updatedAt: new Date().toISOString()
};
posts[postIndex] = updatedPost;
res.json(updatedPost);
},
// Delete a post
deletePost: (req, res) => {
const id = parseInt(req.params.id);
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex === -1) {
return res.status(404).json({ error: 'Post not found' });
}
const deletedPost = posts.splice(postIndex, 1)[0];
res.json({ message: 'Post deleted successfully', post: deletedPost });
}
};
// Routes
app.get('/api/posts', blogController.getAllPosts);
app.get('/api/posts/:id', blogController.getPostById);
app.post('/api/posts', blogController.createPost);
app.put('/api/posts/:id', blogController.updatePost);
app.delete('/api/posts/:id', blogController.deletePost);
// Global error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
// Start server
app.listen(3000, () => {
console.log('Blog API running on port 3000');
});
This example shows a complete API with CRUD operations and organized route handlers.
Summary
Route handlers are the backbone of Express applications, responsible for processing requests and generating responses. Key points to remember:
- Route handlers are functions with the signature
(req, res, next)
- They access request data through
req.params
,req.query
, andreq.body
- They send responses using methods like
res.send()
,res.json()
, andres.status()
- For maintainable code, organize handlers into controller files
- Implement proper error handling using try-catch or the
next()
function - Multiple handlers can be chained for middleware functionality
By mastering route handlers, you'll be able to build robust Express applications with clean, organized code.
Exercises
-
Basic Route Handler: Create a route that returns your name and favorite programming language as JSON.
-
URL Parameters: Create a route
/greet/:name
that returns a greeting with the provided name. -
Query Parameters: Create a route
/calculate
that accepts query parametersa
,b
, andoperation
to perform basic math (addition, subtraction, etc.) -
Multiple Handlers: Create a route with two handlers - one that checks if a user is logged in (based on a query parameter) and another that returns protected data.
-
Controller Organization: Refactor the blog API example to separate the controller logic into its own file.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)