Back to Course
Routing

Exploring ExpressJS Routing

Routing is the mechanism by which an application responds to client requests for specific endpoints. Express.js provides a powerful, flexible routing system that forms the backbone of every web application. Mastering Express routing is essential for building well-structured, maintainable APIs.

Production Reality: Poorly organized routes lead to unmaintainable code, security vulnerabilities, and difficult debugging. A well-structured routing system is the foundation of a production-ready application.

1. Getting Started with Express

Installation

npm install express

Basic Express Server

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

// Basic route
app.get('/', (req, res) => {
    res.send('Hello, World!');
});

// Start server
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

2. HTTP Methods

Express supports all standard HTTP methods for RESTful API design.

MethodUse CaseExpress Method
GETRetrieve dataapp.get()
POSTCreate new dataapp.post()
PUTReplace data (full update)app.put()
PATCHPartial updateapp.patch()
DELETEDelete dataapp.delete()
OPTIONSGet allowed methodsapp.options()
HEADGet headers onlyapp.head()

CRUD Operations Example

const express = require('express');
const app = express();
app.use(express.json()); // ✅ Parse JSON bodies

// In-memory data store (use database in production)
let users = [
    { id: 1, name: 'John Doe', email: 'john@example.com' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

// ✅ GET all users
app.get('/api/users', (req, res) => {
    res.json(users);
});

// ✅ GET single user by ID
app.get('/api/users/:id', (req, res) => {
    const user = users.find(u => u.id === parseInt(req.params.id));
    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
});

// ✅ POST — Create new user
app.post('/api/users', (req, res) => {
    const { name, email } = req.body;
    if (!name || !email) {
        return res.status(400).json({ error: 'Name and email are required' });
    }
    const newUser = {
        id: users.length + 1,
        name,
        email
    };
    users.push(newUser);
    res.status(201).json(newUser);
});

// ✅ PUT — Replace user (full update)
app.put('/api/users/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const { name, email } = req.body;
    const user = users.find(u => u.id === id);
    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }
    if (!name || !email) {
        return res.status(400).json({ error: 'Name and email are required' });
    }
    user.name = name;
    user.email = email;
    res.json(user);
});

// ✅ PATCH — Partial update
app.patch('/api/users/: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' });
    }
    if (req.body.name) user.name = req.body.name;
    if (req.body.email) user.email = req.body.email;
    res.json(user);
});

// ✅ DELETE — Delete user
app.delete('/api/users/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const index = users.findIndex(u => u.id === id);
    if (index === -1) {
        return res.status(404).json({ error: 'User not found' });
    }
    users.splice(index, 1);
    res.status(204).send();
});

3. Route Parameters & Query Strings

Route Parameters

Route parameters are named segments in the URL path, denoted by :.

// Route parameter :id
app.get('/api/users/:id', (req, res) => {
    const id = req.params.id;
    res.json({ userId: id });
});

// Multiple parameters
app.get('/api/users/:userId/posts/:postId', (req, res) => {
    const { userId, postId } = req.params;
    res.json({ userId, postId });
});

// Optional parameter (with ?)
app.get('/api/users/:id?', (req, res) => {
    if (req.params.id) {
        // Fetch specific user
    } else {
        // Fetch all users
    }
});

Query Strings

Query strings are key-value pairs after the ? in the URL, accessible via req.query.

// URL: /api/users?page=2&limit=10&sort=asc
app.get('/api/users', (req, res) => {
    const { page = 1, limit = 10, sort = 'asc' } = req.query;
    res.json({
        page: parseInt(page),
        limit: parseInt(limit),
        sort,
        users: [] // Your data
    });
});

// ✅ Production: Parse and validate query parameters
app.get('/api/products', (req, res) => {
    const { 
        category, 
        minPrice, 
        maxPrice, 
        inStock = true,
        page = 1,
        limit = 20
    } = req.query;

    // Validate numeric parameters
    const parsedPage = parseInt(page);
    const parsedLimit = parseInt(limit);
    const parsedMinPrice = parseFloat(minPrice);
    const parsedMaxPrice = parseFloat(maxPrice);

    if (isNaN(parsedPage) || parsedPage < 1) {
        return res.status(400).json({ error: 'Invalid page parameter' });
    }
    if (isNaN(parsedLimit) || parsedLimit < 1 || parsedLimit > 100) {
        return res.status(400).json({ error: 'Limit must be between 1 and 100' });
    }

    // Build filter object
    const filter = {};
    if (category) filter.category = category;
    if (parsedMinPrice) filter.minPrice = parsedMinPrice;
    if (parsedMaxPrice) filter.maxPrice = parsedMaxPrice;
    filter.inStock = inStock === 'true';

    // Fetch products with filter
    // ...

    res.json({
        filters: filter,
        pagination: { page: parsedPage, limit: parsedLimit }
    });
});

4. Middleware — The Express Superpower

Middleware is a function that has access to the request, response, and the next middleware function in the application's request-response cycle.

What Middleware Can Do

  • Execute any code
  • Modify the request or response
  • End the request-response cycle
  • Call the next middleware in the stack

Types of Middleware

TypeDescriptionExample
Application-levelApplied to the entire appapp.use(express.json())
Router-levelApplied to a specific routerrouter.use(authMiddleware)
Error-handlingHandles errors (4 parameters)app.use((err, req, res, next) => {})
Built-inProvided by Expressexpress.json(), express.static()

Custom Middleware Examples

// ✅ 1. Logging middleware
const loggerMiddleware = (req, res, next) => {
    const start = Date.now();
    res.on('finish', () => {
        const duration = Date.now() - start;
        console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
    });
    next();
};
app.use(loggerMiddleware);

// ✅ 2. Authentication middleware
const authMiddleware = (req, res, next) => {
    const token = req.headers.authorization;
    if (!token) {
        return res.status(401).json({ error: 'Authentication required' });
    }
    try {
        // Verify token (JWT)
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded; // Attach user to request
        next();
    } catch (err) {
        return res.status(401).json({ error: 'Invalid token' });
    }
};
app.use('/api/admin', authMiddleware);

// ✅ 3. Rate limiting middleware
const rateLimitMap = new Map();
const RATE_LIMIT_WINDOW = 60000; // 1 minute
const MAX_REQUESTS = 100;

const rateLimitMiddleware = (req, res, next) => {
    const ip = req.ip || req.connection.remoteAddress;
    const now = Date.now();
    const windowStart = now - RATE_LIMIT_WINDOW;

    if (!rateLimitMap.has(ip)) {
        rateLimitMap.set(ip, []);
    }

    const requests = rateLimitMap.get(ip).filter(time => time > windowStart);
    if (requests.length >= MAX_REQUESTS) {
        return res.status(429).json({ 
            error: 'Too many requests. Please try again later.' 
        });
    }

    requests.push(now);
    rateLimitMap.set(ip, requests);
    next();
};
app.use(rateLimitMiddleware);

// ✅ 4. Error handling middleware (must be LAST)
const errorHandlerMiddleware = (err, req, res, next) => {
    console.error('❌ Error:', err.stack);
    const status = err.status || 500;
    const message = err.message || 'Internal server error';
    res.status(status).json({
        error: message,
        timestamp: new Date().toISOString(),
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    });
};
app.use(errorHandlerMiddleware);

5. Route Organization — Using Express.Router

For production applications, routes should be organized into separate modules using express.Router().

Project Structure

project/
├── src/
│   ├── routes/
│   │   ├── index.js          # Main router
│   │   ├── users.js          # User routes
│   │   ├── products.js       # Product routes
│   │   └── auth.js           # Auth routes
│   ├── controllers/
│   │   ├── userController.js
│   │   └── authController.js
│   ├── middleware/
│   │   ├── auth.js
│   │   └── validation.js
│   └── app.js                # Main app
├── server.js                 # Entry point
└── package.json

Router Module Example

// routes/users.js
const express = require('express');
const router = express.Router();
const { 
    getUsers, 
    getUserById, 
    createUser, 
    updateUser, 
    deleteUser 
} = require('../controllers/userController');
const { authMiddleware } = require('../middleware/auth');
const { validateUser } = require('../middleware/validation');

// Routes
router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', validateUser, createUser);
router.put('/:id', authMiddleware, validateUser, updateUser);
router.delete('/:id', authMiddleware, deleteUser);

module.exports = router;

Controller Example

// controllers/userController.js
const User = require('../models/User');

const getUsers = async (req, res, next) => {
    try {
        const users = await User.find();
        res.json(users);
    } catch (err) {
        next(err);
    }
};

const getUserById = async (req, res, next) => {
    try {
        const user = await User.findById(req.params.id);
        if (!user) {
            return res.status(404).json({ error: 'User not found' });
        }
        res.json(user);
    } catch (err) {
        next(err);
    }
};

const createUser = async (req, res, next) => {
    try {
        const user = new User(req.body);
        await user.save();
        res.status(201).json(user);
    } catch (err) {
        next(err);
    }
};

const updateUser = async (req, res, next) => {
    try {
        const user = await User.findByIdAndUpdate(
            req.params.id,
            req.body,
            { new: true, runValidators: true }
        );
        if (!user) {
            return res.status(404).json({ error: 'User not found' });
        }
        res.json(user);
    } catch (err) {
        next(err);
    }
};

const deleteUser = async (req, res, next) => {
    try {
        const user = await User.findByIdAndDelete(req.params.id);
        if (!user) {
            return res.status(404).json({ error: 'User not found' });
        }
        res.status(204).send();
    } catch (err) {
        next(err);
    }
};

module.exports = {
    getUsers,
    getUserById,
    createUser,
    updateUser,
    deleteUser
};

Main App — Using Routers

// app.js
const express = require('express');
const app = express();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
const authRoutes = require('./routes/auth');

app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
app.use('/api/auth', authRoutes);

// Health check
app.get('/health', (req, res) => {
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// 404 handler
app.use('*', (req, res) => {
    res.status(404).json({ error: 'Route not found' });
});

// Error handler
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(err.status || 500).json({
        error: err.message || 'Internal server error'
    });
});

module.exports = app;

6. Advanced Routing Patterns

Chainable Route Handlers

app.route('/api/users')
    .get((req, res) => { /* GET users */ })
    .post((req, res) => { /* POST user */ });

app.route('/api/users/:id')
    .get((req, res) => { /* GET user */ })
    .put((req, res) => { /* PUT user */ })
    .delete((req, res) => { /* DELETE user */ });

Route Groups with Middleware

// Apply middleware to a group of routes
app.use('/api/admin', authMiddleware, (req, res, next) => {
    // Check if user is admin
    if (!req.user.isAdmin) {
        return res.status(403).json({ error: 'Admin access required' });
    }
    next();
});

// These routes all require authentication + admin access
app.get('/api/admin/users', adminController.getUsers);
app.delete('/api/admin/users/:id', adminController.deleteUser);

Parameter Validation Middleware

// middleware/validation.js
const { body, param, validationResult } = require('express-validator');

const validateUser = [
    body('name').notEmpty().withMessage('Name is required'),
    body('email').isEmail().withMessage('Valid email is required'),
    (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        next();
    }
];

const validateUserId = [
    param('id').isInt().withMessage('ID must be an integer'),
    (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        next();
    }
];

module.exports = { validateUser, validateUserId };

7. Common Production Issues & Solutions

These are real production issues in Express routing.

Issue 1: Routes Order Matters

// ❌ Wrong order — this catches everything!
app.get('/api/users/:id', (req, res) => { /* ... */ });
app.get('/api/users/profile', (req, res) => { /* ... */ });
// Request to /api/users/profile will match the :id route!

// ✅ Correct order — specific routes first
app.get('/api/users/profile', (req, res) => { /* ... */ });
app.get('/api/users/:id', (req, res) => { /* ... */ });
// Request to /api/users/profile matches the specific route first

Issue 2: Async Error Handling

// ❌ Unhandled promise rejection
app.get('/api/users', async (req, res) => {
    const users = await User.find(); // If this fails, the app crashes
    res.json(users);
});

// ✅ Wrap async handlers with error handler
const asyncHandler = (fn) => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/api/users', asyncHandler(async (req, res) => {
    const users = await User.find();
    res.json(users);
}));

// ✅ Or use express-async-errors
require('express-async-errors');
app.get('/api/users', async (req, res) => {
    const users = await User.find();
    res.json(users);
});

Issue 3: CORS Configuration

// ✅ Production CORS setup
const cors = require('cors');
const allowedOrigins = ['https://myapp.com', 'https://api.myapp.com'];

app.use(cors({
    origin: (origin, callback) => {
        if (!origin || allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
    maxAge: 86400 // 24 hours
}));

8. Production-Ready Checklist

  • ✅ Use express.Router() — Organize routes into separate modules.
  • ✅ Use controllers — Separate route definitions from business logic.
  • ✅ Implement validation — Validate request data before processing.
  • ✅ Handle errors — Use error-handling middleware.
  • ✅ Use async/await — With proper error handling.
  • ✅ Order routes correctly — Place specific routes before parameter routes.
  • ✅ Add security middleware — Helmet.js, CORS, rate limiting.
  • ✅ Log requests — Implement request logging middleware.
  • ✅ Use environment variables — For configuration.
  • ✅ Add health checks — Expose /health endpoint.
  • ✅ Implement CORS — Properly configure for production.
  • ✅ Add request validation — Use express-validator or Joi.

9. Quick Reference — Express Routing

ConceptSyntaxExample
GET Routeapp.get(path, handler)app.get('/users', getUsers)
POST Routeapp.post(path, handler)app.post('/users', createUser)
Route Parameter:paramapp.get('/users/:id', getUser)
Query Stringreq.queryconst { page } = req.query
Request Bodyreq.bodyconst { name } = req.body
Routerexpress.Router()const router = express.Router()
Middlewareapp.use(fn)app.use(express.json())
Error Handlerapp.use((err, req, res, next) => {})4 parameters required
Chained Routesapp.route(path).get().post()app.route('/users').get(getUsers).post(createUser)
Production Wisdom: Well-organized routes are the foundation of a maintainable Express application. Use the Router pattern early, even in small projects — it scales naturally and makes your codebase cleaner and more testable. The time invested in structuring your routes properly will pay off in reduced debugging time and easier onboarding of new team members.

Ready to master Node.js?

Build scalable, high-performance backend applications with Node.js. Learn from industry experts with hands-on projects.

Explore Course