Skip to main content

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:

javascript
function(req, res, next) {
// Your logic here
}

Where:

  • req is the request object containing information about the HTTP request
  • res is the response object used to send back data to the client
  • next 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:

javascript
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:

javascript
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:

javascript
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):

javascript
app.get('/profile', (req, res) => {
res.render('profile', {
username: 'JohnDoe',
isAdmin: true
});
});

res.status()

Sets the HTTP status code (chainable with other methods):

javascript
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:

javascript
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:

javascript
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):

javascript
// 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:

json
{
"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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
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:

javascript
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:

javascript
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, and req.body
  • They send responses using methods like res.send(), res.json(), and res.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

  1. Basic Route Handler: Create a route that returns your name and favorite programming language as JSON.

  2. URL Parameters: Create a route /greet/:name that returns a greeting with the provided name.

  3. Query Parameters: Create a route /calculate that accepts query parameters a, b, and operation to perform basic math (addition, subtraction, etc.)

  4. 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.

  5. 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! :)