Node.js & Express

Express Routing

22 min Lesson 8 of 40

Understanding Routing in Express

Routing refers to how an application responds to client requests to specific endpoints (URIs) combined with specific HTTP methods (GET, POST, PUT, DELETE, etc.). Express provides a powerful and flexible routing system that makes it easy to organize your application's endpoints and handle different types of requests.

Basic Route Structure

A route definition has the following structure:

app.METHOD(PATH, HANDLER)

Where:

  • app is an instance of Express
  • METHOD is an HTTP request method (get, post, put, delete, etc.) in lowercase
  • PATH is the path/endpoint on the server
  • HANDLER is the function executed when the route is matched

HTTP Route Methods

Express supports all standard HTTP methods. Here are the most commonly used ones:

GET - Retrieve Data

GET requests are used to retrieve data from the server:

// Get all users app.get('/users', (req, res) => { res.json([ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ]); }); // Get a specific user app.get('/users/:id', (req, res) => { const userId = req.params.id; res.json({ id: userId, name: 'John' }); });

POST - Create Data

POST requests are used to create new resources:

app.post('/users', (req, res) => { const { name, email } = req.body; // In real app, save to database res.status(201).json({ id: 3, name, email, message: 'User created successfully' }); });

PUT - Update Data (Complete Replacement)

PUT requests replace the entire resource:

app.put('/users/:id', (req, res) => { const userId = req.params.id; const { name, email, age } = req.body; // In real app, update in database res.json({ id: userId, name, email, age, message: 'User updated completely' }); });

PATCH - Update Data (Partial Update)

PATCH requests update only specific fields:

app.patch('/users/:id', (req, res) => { const userId = req.params.id; const updates = req.body; // Only fields to update res.json({ id: userId, ...updates, message: 'User updated partially' }); });

DELETE - Remove Data

DELETE requests remove resources:

app.delete('/users/:id', (req, res) => { const userId = req.params.id; // In real app, delete from database res.json({ message: `User ${userId} deleted successfully` }); });

All Methods

Handle all HTTP methods for a route:

app.all('/secret', (req, res) => { console.log(`Accessing secret section via ${req.method}`); res.send('Secret area'); });
Note: The order in which you define routes matters. Express matches routes in the order they are defined. Put more specific routes before generic ones.

Route Parameters

Route parameters are named URL segments used to capture values at specific positions in the URL. The captured values are stored in the req.params object.

Single Parameter

app.get('/users/:id', (req, res) => { const userId = req.params.id; res.send(`User ID: ${userId}`); }); // Example: /users/123 → User ID: 123

Multiple Parameters

app.get('/users/:userId/posts/:postId', (req, res) => { const { userId, postId } = req.params; res.json({ user: userId, post: postId, message: `Getting post ${postId} from user ${userId}` }); }); // Example: /users/5/posts/42 // Result: { user: '5', post: '42', message: ... }

Parameter Patterns

You can use regular expressions to validate parameters:

// Only match numeric IDs app.get('/products/:id(\\d+)', (req, res) => { res.send(`Product ID: ${req.params.id}`); }); // Match IDs with exactly 5 digits app.get('/orders/:orderId(\\d{5})', (req, res) => { res.send(`Order ID: ${req.params.orderId}`); });

Optional Parameters

// The ? makes the parameter optional app.get('/users/:id?/profile', (req, res) => { if (req.params.id) { res.send(`Profile for user ${req.params.id}`); } else { res.send('Profile for current user'); } }); // Matches both: // /users/123/profile // /users/profile
Tip: Always validate and sanitize route parameters before using them, especially when querying databases to prevent injection attacks.

Query String Parameters

Query strings are key-value pairs that appear after the ? in a URL. They are accessed via req.query.

Single Query Parameter

app.get('/search', (req, res) => { const searchTerm = req.query.q; res.json({ query: searchTerm, results: [`Result 1 for ${searchTerm}`, `Result 2 for ${searchTerm}`] }); }); // Example: /search?q=express // Result: { query: 'express', results: [...] }

Multiple Query Parameters

app.get('/products', (req, res) => { const { category, minPrice, maxPrice, sort } = req.query; res.json({ category: category || 'all', priceRange: { min: minPrice || 0, max: maxPrice || Infinity }, sortBy: sort || 'relevance' }); }); // Example: /products?category=electronics&minPrice=100&maxPrice=500&sort=price

Default Values and Type Conversion

app.get('/items', (req, res) => { // Query parameters are always strings const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const sortOrder = req.query.sort || 'asc'; res.json({ page, limit, sortOrder, message: `Page ${page} with ${limit} items per page` }); });
Note: Query parameters are always received as strings. You need to convert them to numbers or booleans if needed.

Router Module

For larger applications, it's better to organize routes into separate modules using express.Router(). This creates modular, mountable route handlers.

Creating a Router

Create a file routes/users.js:

const express = require('express'); const router = express.Router(); // All routes here are relative to the mount point // GET /users router.get('/', (req, res) => { res.json({ message: 'Get all users' }); }); // GET /users/:id router.get('/:id', (req, res) => { res.json({ message: `Get user ${req.params.id}` }); }); // POST /users router.post('/', (req, res) => { res.status(201).json({ message: 'User created' }); }); // PUT /users/:id router.put('/:id', (req, res) => { res.json({ message: `Update user ${req.params.id}` }); }); // DELETE /users/:id router.delete('/:id', (req, res) => { res.json({ message: `Delete user ${req.params.id}` }); }); module.exports = router;

Using the Router in Main App

In your main app.js:

const express = require('express'); const app = express(); const usersRouter = require('./routes/users'); app.use(express.json()); // Mount the router at /users app.use('/users', usersRouter); // Now all routes in usersRouter are prefixed with /users // GET /users → router.get('/') // GET /users/123 → router.get('/:id') // POST /users → router.post('/') app.listen(3000);

Multiple Routers

You can create multiple routers for different resources:

// routes/users.js const usersRouter = require('./routes/users'); // routes/posts.js const postsRouter = require('./routes/posts'); // routes/comments.js const commentsRouter = require('./routes/comments'); // app.js app.use('/api/users', usersRouter); app.use('/api/posts', postsRouter); app.use('/api/comments', commentsRouter);

Nested Routers

You can nest routers to create hierarchical route structures:

// routes/users.js const express = require('express'); const router = express.Router(); const postsRouter = require('./posts'); router.get('/', (req, res) => { res.json({ message: 'All users' }); }); // Nest posts router under users router.use('/:userId/posts', postsRouter); module.exports = router;
// routes/posts.js const express = require('express'); const router = express.Router({ mergeParams: true }); // Important! router.get('/', (req, res) => { // Access parent route parameter const userId = req.params.userId; res.json({ message: `Posts for user ${userId}` }); }); router.get('/:postId', (req, res) => { const { userId, postId } = req.params; res.json({ message: `Post ${postId} from user ${userId}` }); }); module.exports = router;
// app.js app.use('/users', usersRouter); // Results in routes: // GET /users → All users // GET /users/5/posts → Posts for user 5 // GET /users/5/posts/42 → Post 42 from user 5
Warning: When using nested routers, set mergeParams: true in the child router to access parent route parameters. Without this, req.params.userId would be undefined in the posts router.

Route Grouping and Chaining

You can chain route handlers for the same path:

Method Chaining

router.route('/users/:id') .get((req, res) => { res.json({ message: 'Get user' }); }) .put((req, res) => { res.json({ message: 'Update user' }); }) .delete((req, res) => { res.json({ message: 'Delete user' }); });

Multiple Handlers for One Route

You can pass multiple callback functions as handlers:

// Middleware functions const validateUser = (req, res, next) => { if (!req.body.name) { return res.status(400).json({ error: 'Name is required' }); } next(); }; const checkAuth = (req, res, next) => { // Check authentication const isAuthenticated = true; // Simplified if (!isAuthenticated) { return res.status(401).json({ error: 'Unauthorized' }); } next(); }; // Apply multiple handlers router.post('/users', checkAuth, validateUser, (req, res) => { res.status(201).json({ message: 'User created' }); });

Array of Handlers

const handlers = [ checkAuth, validateUser, (req, res) => { res.json({ message: 'All checks passed' }); } ]; router.post('/users', handlers);

Path Patterns

Express supports various patterns for route paths:

String Patterns

// Matches any path starting with /ab app.get('/ab*cd', (req, res) => { res.send('Matched: abcd, abxcd, ab123cd, etc.'); }); // The + means one or more app.get('/ab+cd', (req, res) => { res.send('Matches: abcd, abbcd, abbbcd, etc.'); }); // The ? means optional app.get('/ab?cd', (req, res) => { res.send('Matches: acd, abcd'); });

Regular Expression Patterns

// Match any route containing 'fly' app.get(/.*fly$/, (req, res) => { res.send('Matches: butterfly, dragonfly, etc.'); }); // Match exactly 3 digits app.get(/^\/\d{3}$/, (req, res) => { res.send('Matches: /123, /456, etc.'); });

Organizing Routes Best Practices

Recommended Folder Structure

project/ ├── app.js ├── routes/ │ ├── index.js // Main router file │ ├── users.js │ ├── posts.js │ └── auth.js ├── controllers/ │ ├── usersController.js │ ├── postsController.js │ └── authController.js └── middleware/ ├── auth.js └── validation.js

Separating Controllers

Keep route logic in separate controller files:

// controllers/usersController.js exports.getAllUsers = (req, res) => { res.json({ message: 'Get all users' }); }; exports.getUserById = (req, res) => { res.json({ message: `Get user ${req.params.id}` }); }; exports.createUser = (req, res) => { res.status(201).json({ message: 'User created' }); }; exports.updateUser = (req, res) => { res.json({ message: 'User updated' }); }; exports.deleteUser = (req, res) => { res.json({ message: 'User deleted' }); };
// routes/users.js const express = require('express'); const router = express.Router(); const usersController = require('../controllers/usersController'); router.get('/', usersController.getAllUsers); router.get('/:id', usersController.getUserById); router.post('/', usersController.createUser); router.put('/:id', usersController.updateUser); router.delete('/:id', usersController.deleteUser); module.exports = router;
Tip: Use descriptive route names and follow RESTful conventions for API routes. For example: GET /users, POST /users, GET /users/:id, PUT /users/:id, DELETE /users/:id

RESTful Routing Conventions

RESTful APIs follow standard conventions for route naming:

// Resource: users GET /users // Get all users GET /users/:id // Get specific user POST /users // Create new user PUT /users/:id // Update entire user PATCH /users/:id // Update partial user DELETE /users/:id // Delete user // Nested resources GET /users/:userId/posts // Get posts by user GET /users/:userId/posts/:postId // Get specific post by user POST /users/:userId/posts // Create post for user
Exercise: Build a complete RESTful API for a blog:
  1. Create a project structure with separate route and controller files
  2. Implement routes for three resources: users, posts, and comments
  3. Users routes:
    • GET /api/users - Get all users
    • GET /api/users/:id - Get user by ID
    • POST /api/users - Create user
    • PUT /api/users/:id - Update user
    • DELETE /api/users/:id - Delete user
  4. Posts routes (similar structure to users)
  5. Nested route: GET /api/users/:userId/posts - Get all posts by user
  6. Implement query parameters for pagination: /api/posts?page=1&limit=10
  7. Create validation middleware to check required fields
  8. Test all routes using Postman or Thunder Client
  9. Add a 404 handler for undefined routes