Express API Routing
Introduction
Routing is one of the fundamental concepts in building Express applications, particularly REST APIs. Routing refers to how an application's endpoints (URIs) respond to client requests. Whether you're building a simple API or a complex web service, understanding how to structure and organize your routes is essential for creating maintainable and scalable applications.
In this guide, we'll explore Express routing in depth, covering basic routes, route parameters, query parameters, and best practices for organizing your API routes.
Basic Routing in Express
At its core, Express routing defines how your application responds to client requests to specific endpoints, which are defined by URLs (or paths) and HTTP methods (GET, POST, PUT, DELETE, etc.).
Let's start with a simple example:
const express = require('express');
const app = express();
const port = 3000;
// Basic route
app.get('/', (req, res) => {
res.send('Hello World!');
});
// Starting the server
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
In this example:
- We've created a simple Express application
- We've defined a route that responds to GET requests at the root path ('/')
- When a client makes a GET request to this path, the server responds with "Hello World!"
HTTP Methods
Express supports all HTTP methods. Here are examples of the most common ones:
// GET request - Retrieve data
app.get('/users', (req, res) => {
res.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
});
// POST request - Create data
app.post('/users', (req, res) => {
// Create a new user
res.status(201).send('User created');
});
// PUT request - Update data
app.put('/users/:id', (req, res) => {
// Update user with the specified ID
res.send(`User ${req.params.id} updated`);
});
// DELETE request - Delete data
app.delete('/users/:id', (req, res) => {
// Delete user with the specified ID
res.send(`User ${req.params.id} deleted`);
});
These HTTP methods correspond to CRUD operations (Create, Read, Update, Delete) which are fundamental to REST API design.
Route Parameters
Often, you'll need to capture values from the URL. For example, when retrieving a specific user by ID, you might use a URL like /users/123
. Express allows you to define route parameters using a colon syntax:
app.get('/users/:id', (req, res) => {
// The value of the 'id' parameter is accessible via req.params.id
const userId = req.params.id;
res.send(`Fetching details for user with ID: ${userId}`);
});
You can define multiple route parameters:
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.send(`Fetching post ${postId} by user ${userId}`);
});
Example: Route Parameters in Action
Let's say we're building a book API. We might have routes like:
// Get all books
app.get('/books', (req, res) => {
res.json([
{ id: 1, title: 'The Great Gatsby' },
{ id: 2, title: 'To Kill a Mockingbird' }
]);
});
// Get a specific book by ID
app.get('/books/:id', (req, res) => {
const bookId = req.params.id;
// In a real app, you would fetch the book from a database
const book = { id: bookId, title: 'Sample Book Title' };
res.json(book);
});
// Get all reviews for a specific book
app.get('/books/:id/reviews', (req, res) => {
const bookId = req.params.id;
// Fetch reviews for the specified book
const reviews = [
{ id: 1, rating: 5, text: 'Excellent book!' },
{ id: 2, rating: 4, text: 'Really enjoyed it.' }
];
res.json(reviews);
});
Query Parameters
Query parameters provide another way to pass data to your API. They appear after a question mark (?) in the URL, like /search?q=express&limit=10
.
Here's how to access query parameters in Express:
app.get('/search', (req, res) => {
const query = req.query.q;
const limit = req.query.limit || 10; // Default to 10 if not provided
res.send(`Searching for "${query}" with limit ${limit}`);
});
If a user makes a request to /search?q=express&limit=5
, the server would respond with: "Searching for "express" with limit 5".
Query parameters are especially useful for:
- Filtering results (e.g.,
/products?category=electronics
) - Pagination (e.g.,
/products?page=2&limit=20
) - Sorting (e.g.,
/products?sort=price&order=desc
)
Example: Implementing Pagination
Here's a more complete example of using query parameters for pagination:
app.get('/products', (req, res) => {
// Extract query parameters with defaults
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const category = req.query.category || 'all';
// Calculate starting index for pagination
const startIndex = (page - 1) * limit;
// In a real app, you would query your database with these parameters
const results = {
metadata: {
page,
limit,
category
},
products: [
{ id: startIndex + 1, name: 'Sample Product 1' },
{ id: startIndex + 2, name: 'Sample Product 2' },
// More products...
]
};
res.json(results);
});
Route Handlers
Route handlers are the functions executed when a matching route is found. You can define multiple handlers for a single route:
// Multiple handlers for a single route
app.get('/example',
(req, res, next) => {
// First middleware function
console.log('First handler');
next(); // Pass control to the next handler
},
(req, res) => {
// Second handler function
res.send('Response from the second handler');
}
);
This is useful for implementing middleware that should run before the main route logic, such as authentication, logging, or data validation.
Express Router
As your application grows, having all routes in a single file becomes unwieldy. Express provides a Router class to create modular, mountable route handlers:
// users.js
const express = require('express');
const router = express.Router();
// Define routes for the users resource
router.get('/', (req, res) => {
res.json([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]);
});
router.get('/:id', (req, res) => {
res.json({ id: req.params.id, name: 'User name' });
});
router.post('/', (req, res) => {
res.status(201).send('User created');
});
module.exports = router;
Then in your main app file:
const express = require('express');
const app = express();
const usersRouter = require('./users');
// Mount the router at /users
app.use('/users', usersRouter);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
With this setup, the router routes become:
/users
- GET request returns all users/users/:id
- GET request returns a specific user/users
- POST request creates a new user
Organizing Routes by Resource
A common practice in REST API design is to organize routes by resource. Here's an example structure:
// File: routes/books.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
// Get all books
});
router.post('/', (req, res) => {
// Create a new book
});
router.get('/:id', (req, res) => {
// Get a specific book
});
router.put('/:id', (req, res) => {
// Update a book
});
router.delete('/:id', (req, res) => {
// Delete a book
});
module.exports = router;
// File: routes/authors.js
const express = require('express');
const router = express.Router();
// Similar route definitions for authors...
module.exports = router;
// File: app.js
const express = require('express');
const app = express();
const booksRouter = require('./routes/books');
const authorsRouter = require('./routes/authors');
app.use('/books', booksRouter);
app.use('/authors', authorsRouter);
app.listen(3000);
This structure makes your API more maintainable as it grows.
Route Middleware
Middleware functions have access to the request and response objects, and the next
function in the application's request-response cycle. They can:
- Execute any code
- Modify the request and response objects
- End the request-response cycle
- Call the next middleware function
Here's an example of a route-specific middleware that checks if a user is authenticated:
// Authentication middleware
const authenticate = (req, res, next) => {
const authToken = req.headers.authorization;
if (!authToken) {
return res.status(401).json({ message: 'Authentication required' });
}
// In a real app, you would validate the token
// If valid, attach the user info to the request object
req.user = { id: 123, name: 'Authenticated User' };
// Call next() to pass control to the next middleware function
next();
};
// Use the middleware for specific routes
app.get('/profile', authenticate, (req, res) => {
res.json({
message: 'Profile access granted',
user: req.user
});
});
// Public route - no authentication needed
app.get('/public', (req, res) => {
res.send('This is a public route');
});
Error Handling in Routes
Express has a special kind of middleware for handling errors. Error-handling middleware takes four arguments instead of three (error, request, response, next):
app.get('/data', (req, res, next) => {
try {
// Simulating an error
const randomNumber = Math.random();
if (randomNumber > 0.5) {
throw new Error('Something went wrong');
}
res.json({ data: 'Everything is fine' });
} catch (error) {
// Pass the error to the error handler
next(error);
}
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: err.message || 'Internal Server Error'
});
});
Versioning Your API
As your API evolves, you may need to introduce changes that aren't backward-compatible. API versioning helps manage these changes:
// Version 1 of the API
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
res.json([{ name: 'User 1' }, { name: 'User 2' }]);
});
// Version 2 of the API with additional fields
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
res.json([
{ name: 'User 1', email: '[email protected]' },
{ name: 'User 2', email: '[email protected]' }
]);
});
// Mount the versioned routers
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Building a Complete REST API Example
Let's put everything together in a more complete example of a todos API:
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// In-memory database (replace with a real DB in production)
let todos = [
{ id: 1, title: 'Learn Express', completed: false },
{ id: 2, title: 'Build an API', completed: true }
];
// GET all todos
app.get('/api/todos', (req, res) => {
// Support filtering by completion status
const { completed } = req.query;
if (completed === 'true') {
return res.json(todos.filter(todo => todo.completed));
} else if (completed === 'false') {
return res.json(todos.filter(todo => !todo.completed));
}
res.json(todos);
});
// GET a single todo by ID
app.get('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find(todo => todo.id === id);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
res.json(todo);
});
// POST a new todo
app.post('/api/todos', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ message: 'Title is required' });
}
const newTodo = {
id: todos.length + 1,
title,
completed: false
};
todos.push(newTodo);
res.status(201).json(newTodo);
});
// PUT to update a todo
app.put('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(todo => todo.id === id);
if (todoIndex === -1) {
return res.status(404).json({ message: 'Todo not found' });
}
const { title, completed } = req.body;
// Update only the provided fields
todos[todoIndex] = {
...todos[todoIndex],
...(title !== undefined && { title }),
...(completed !== undefined && { completed })
};
res.json(todos[todoIndex]);
});
// DELETE a todo
app.delete('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(todo => todo.id === id);
if (todoIndex === -1) {
return res.status(404).json({ message: 'Todo not found' });
}
const deletedTodo = todos[todoIndex];
todos = todos.filter(todo => todo.id !== id);
res.json(deletedTodo);
});
// Start the server
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
This example demonstrates:
- Basic CRUD operations
- Route parameters
- Query parameters
- Request body handling
- Error handling
- Status codes
- RESTful route structure
Best Practices for API Routing
-
Use Nouns, Not Verbs: Use resource names (nouns) in URLs instead of actions (verbs).
- Good:
/users
(to get users) - Avoid:
/getUsers
- Good:
-
Use Plural Resource Names: This makes your API more consistent.
- Good:
/users
,/products
- Avoid:
/user
,/product
- Good:
-
Use Proper HTTP Methods:
- GET for reading
- POST for creating
- PUT or PATCH for updating
- DELETE for deleting
-
Return Appropriate Status Codes:
- 200 OK for successful GET, PUT, PATCH
- 201 Created for successful POST
- 204 No Content for successful DELETE
- 400 Bad Request for client errors
- 401/403 for authentication/authorization errors
- 404 Not Found when resources don't exist
- 500 Internal Server Error for server issues
-
Implement Pagination: For endpoints that return many items.
-
Version Your API: Include version information in the URL (e.g.,
/api/v1/users
). -
Use Query Parameters for Filtering, Sorting, and Pagination:
/products?category=electronics&sort=price&order=asc&page=2&limit=20
-
Use Consistent Error Formats: Return consistent error objects.
Summary
In this guide, we've covered Express API routing fundamentals:
- Basic routing with HTTP methods (GET, POST, PUT, DELETE)
- Route parameters for capturing values from URLs
- Query parameters for filtering, sorting, and pagination
- Route handlers and middleware
- Express Router for organizing routes
- Error handling in routes
- API versioning strategies
- Best practices for REST API design
Understanding these concepts is crucial for building well-structured and maintainable Express APIs. With proper routing, your API becomes more intuitive, predictable, and easier to work with, both for you and for the developers consuming your API.
Additional Resources
Exercises
- Create a basic Express API with routes for managing a collection of products.
- Implement CRUD operations for a "tasks" resource.
- Add query parameters to filter tasks by completion status and creation date.
- Implement a /api/v1 prefix for all routes to support versioning.
- Add error handling for invalid IDs and missing required fields.
- Implement pagination for endpoints that return multiple items.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)