Sitemap

Mastering Express Router: The Complete Guide to Route Handling in Node.js

(Node Series Part 9)

In this section, I have covered how to work with the Router in Express.js

Press enter or click to view image in full size

Routing in Express.js

Routing is one of the core features of Express.js, determining how an application responds to client requests for specific endpoints (URIs) and HTTP methods (GET, POST, etc.).

Basically helps us avoid writing the same end point multiple times and consolidate the API calls for that endpoint as a single module and handle it better.

The general syntax for routes in Express is:

app.METHOD(PATH, HANDLER)

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

How Routing Works

Request Matching: When a request comes in, Express matches:

  • The HTTP method (GET, POST, etc.)
  • The URL path

Route Execution: The first matching route is executed. Routes are matched in the order they are defined.

Handler Execution: The route handler function receives:

  • req (request object) - contains request data
  • res (response object) - used to send a response
  • next (optional) - passes control to the next middleware

Route Paths

Route paths can be:

  • Strings: Exact matches ('/about')
  • String patterns: Wildcards ('/ab*cd' matches 'abcd', 'abxcd', etc.)
  • Regular expressions: (/\/user\/([0-9]+)/)
  • Route parameters: ('/users/:userId')

Route Handlers

Handlers can be:

  • A single function
//single function
app.get('/greet', (req, res) => {
res.send('Hello there!');
});
  • Multiple functions,we need to call the next method to call the next handler.
//Multiple Callbacks 
// Middleware function
const logVisit = (req, res, next) => {
console.log('Someone visited!');
next(); // Pass to next handler
};

// Route with two handlers
app.get('/about',
logVisit,
(req, res) => {
console.log('About Us page');
res.send('About Us page');
}
);
Press enter or click to view image in full size

What it does: Chains multiple functions that execute sequentially

When to use: When you need middleware (like auth checks, logging, data validation)

Flow: Request → Middleware → Handler → Response

Get Shubham Gupta’s stories in your inbox

Join Medium for free to get updates from this writer.

Key point: Must call next() to continue to next handler

  • Combinations of functions and arrays
// Two middleware functions
const detectBrowser = (req, res, next) => {
console.log('Browser:', req.headers['user-agent']);
next();
};

const checkMobile = (req, res, next) => {
req.isMobile = /mobile/i.test(req.headers['user-agent']);
next();
};
// Route using array
app.get('/products',
[detectBrowser, checkMobile],
(req, res) => {
if (req.isMobile) {
res.send('Mobile products page');
} else {
res.send('Desktop products page');
}
}
);
Press enter or click to view image in full size
Press enter or click to view image in full size

Express Router

Express Router is a mini Express application that provides route organization and modularity. It helps you:

  • Split routes into multiple files
  • Organize by features or resource types
  • Apply middleware to specific route groups

Basic Router Setup

Create a file user inside the routes folder

// routes/users.js
import express from 'express';
const router = express.Router();

// Define routes
router.get('/', (req, res) => {
res.send('User list');
});
router.get('/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
export default router;

inside express.js

import express from "express";
import cookieParser from "cookie-parser";
import userRouter from './routes/users.js';

const app = express();
app.use(express.json());
app.use(cookieParser());
//mounting router
app.use('/users', userRouter);

app.listen(3000, serverListener);
function serverListener(err) {
if (err) {
console.log("error", err);
}else{
console.log("Server Started");
}
}
GET  /users  HTTP/1.1
Host: localhost:3000

###
GET /users/12 HTTP/1.1
Host: localhost:3000
Press enter or click to view image in full size

Key Router Features

Below are some router features

Route Parameters: Capture dynamic values from the URL path

router.get('/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});

router.get('/:id/:name', (req, res) => {
res.send(`User ID: ${req.params.id} User Name:${req.params.name}`);
});
GET  /users/12/shubham  HTTP/1.1
Host: localhost:3000
Press enter or click to view image in full size

Router-Level Middleware: Middleware that runs only for routes in this router

router.use((req, res, next) => {
console.log(req.url);
next();
});
import express from 'express';
const router = express.Router();

router.use((req, res, next) => {
console.log(req.url);
next();
});
router.get('/', (req, res) => {
console.log('User list');

res.send('User list');
});
router.get('/:id', (req, res) => {
console.log(`User ID: ${req.params.id}`);
res.send(`User ID: ${req.params.id}`);
});
router.post('/create', (req, res) => {
console.log(`Create User`);
res.send(`Create User`);
});
export default router;
Press enter or click to view image in full size

HTTP Method Handlers:Support for all HTTP methods (GET, POST, PUT, etc.)

//users.js
import express from 'express';
const router = express.Router();

router.get('/', (req, res) => {
console.log('User list');

res.send('User list');
});
router.get('/:id', (req, res) => {
console.log(`User ID: ${req.params.id}`);
res.send(`User ID: ${req.params.id}`);
});
router.get('/:id/:name', (req, res) => {
console.log(`User ID: ${req.params.id} User Name:${req.params.name}`);
res.send(`User ID: ${req.params.id} User Name:${req.params.name}`);
});

router.post('/create', (req, res) => {
console.log(`Create User`);
res.send(`Create User`);
});
router.delete('/:id', (req, res) => {
console.log(`Delete User ${req.params.id}`);
res.send(`Delete User`);
});

export default router;
GET  /users  HTTP/1.1
Host: localhost:3000

###
GET /users/12 HTTP/1.1
Host: localhost:3000

###
GET /users/12/shubham HTTP/1.1
Host: localhost:3000

###
POST /users/create HTTP/1.1
Host: localhost:3000

###
DELETE /users/12 HTTP/1.1 / HTTP/1.1
Host: localhost:3000
Press enter or click to view image in full size
Press enter or click to view image in full size

Route Chaining: Group same-path routes by method for cleaner code We use route method which has base path to it and then methods are changed to it.

router.route(PATH)
.METHOD(handler)
.METHOD(handler)
// ...additional methods

Here is an example, we will convert the below routes into chaining

import express from 'express';
const router = express.Router();
router.use((req, res, next) => {
console.log(req.url);
next();
});
router.get('/', (req, res) => {
console.log('User list');
res.send('User list');
});
router.get('/:id', (req, res) => {
console.log(`User ID: ${req.params.id} `);
res.send(`User ID: ${req.params.id}`);
});

router.post('/create', (req, res) => {
console.log(`Create User`);
res.send(`Create User`);
});
router.delete('/:id', (req, res) => {
console.log(`Delete User ${req.params.id}`);
res.send(`Delete User`);
});

export default router;

Here are the chained routes

import express from 'express';
const router = express.Router();

// Middleware for all routes
router.use((req, res, next) => {
console.log('Request URL:', req.url);
next();
});
// Base route
router.route('/')
.get((req, res) => {
console.log('User list');
res.send('User list');
});
// ID-based routes
router.route('/:id')
.get((req, res) => {
console.log(`Get User ID: ${req.params.id}`);
res.send(`User ID: ${req.params.id}`);
})
.delete((req, res) => {
console.log(`Delete User ${req.params.id}`);
res.send(`Delete User ${req.params.id}`);
});
// Create route
router.route('/create')
.post((req, res) => {
console.log('Create User');
res.send('Create User');
});
export default router;

Multiple Middleware Stack: Sequence multiple handlers for a single route

const validate = (req, res, next) => { /* validation logic */ };
const sanitize = (req, res, next) => { /* data cleaning */ };

router.post('/submit',
validate,
sanitize,
(req, res) => { /* final handler */ }
);

router.all(): this method is a special routing function that matches all HTTP methods (GET, POST, PUT, DELETE, etc.) for a specified path.

It’s commonly used for:

  • Global middleware for specific paths
  • Catch-all handlers
  • Path-based preprocessing

Important Notes

  • all() Handlers execute before method-specific handlers
  • Always call next() Unless you're intentionally terminating the request
  • Can be combined with other routing methods
  • Useful for creating “gatekeeper” middleware for route groups
router.all(path, callback)

Example

router.all('/products', (req, res, next) => {
console.log('This runs for ALL methods first');
next();
});

router.get('/products', (req, res) => {
res.send('GET products');
});

router.post('/products', (req, res) => {
res.send('POST products');
});

Router Params vs. Static Routes Conflict

Issue: Express matches routes in order, so a static route (e.g., /getall) must be defined before a dynamic parameterized route (e.g., /:id), or the dynamic route will "steal" requests meant for the static route.

  1. Route matching depends on BOTH the path AND the HTTP method
  2. Conflicts only occur for identical methods
  3. Static routes should precede dynamic ones for the same method
// DYNAMIC (matches ANY path, including "/create")
router.get('/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
// STATIC (will NEVER be reached)
router.get('/getall', (req, res) => {
res.send('User List');
});
###
GET /users/getall HTTP/1.1
Host: localhost:3000
Press enter or click to view image in full size

Always define static routes before parameterized routes in your router to avoid conflicts when methods are the same

router.get('/getall', (req, res) => {  
res.send('User List');
});

router.get('/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});

Controllers

Now, as you can see above, our route file seems crowded as all the code for the method will be there, it will be difficult to modify, maintain, and debug, so we introduce controllers.

Controllers in Express.js are functions that handle the business logic for specific routes. They help keep your code organized, modular, and maintainable by separating route definitions from the actual logic.

A controller is a function that:

  • Receives req (request) and res (response) objects.
  • Processes the request (e.g., database queries, validations).
  • Sends back a response (e.g., JSON, HTML, error messages).

Basic Controller Structure

A controller is typically structured as an object or module with methods for different actions.

Here is an example of how to use controllers, create a separate folder for the controllers, and I have added a file user.js, which will have a function for each method

// this is our route file
import express from 'express';
const router = express.Router();

router.use((req, res, next) => {
console.log(req.url);
next();
});


router.get('/getall', (req, res) => {
res.send('User List');
});

router.post('/create', (req, res) => {
console.log(`Create User`);
res.send(`Create User`);
});

router.delete('/:id', (req, res) => {
console.log(`Delete User ${req.params.id}`);
res.send(`Delete User`);
});


export default router;
// this is user.js controller file

export function getHandller(req,res) {
res.send('User List');
}

export function postHandller(req, res) {
console.log(`Create User`);
res.send(`Create User`);
}
export function deleteHandller(req, res) {
console.log(`Delete User ${req.params.id}`);
res.send(`Delete User`);
}

Now our route file will become like this.

import express from 'express';
import * as controller from "../controllers/user"

const router = express.Router();

router.use((req, res, next) => {
console.log(req.url);
next();
});

router.get('/getall', controller.getHandller);
router.post('/create',controller.postHandller);
router.delete('/:id', controller.deleteHandller);


export default router;

Best Practices

  1. Keep controllers focused (one responsibility per method).
  2. Use async/await for database operations.
  3. Handle errors properly (avoid crashing the server).
  4. Use dependency injection for better testing.
  5. Avoid business logic in routes (delegate to controllers).

If you’d like to be in the know, subscribe, clap, like, and share. Cheers!