Node.js 29 🟢 Express Routing, URL Parameters, and Query Strings
A route is a mapping from an HTTP method and a URL pattern to a handler function. Express provides the routing layer that the raw http module does not. The previous chapter introduced the framework and the shape of an application. This chapter goes deeper into routing: the methods, the path patterns, the parameters, the query strings, and the Router object that keeps large applications organized.
Key point: Express routes are defined with app.METHOD(path, handler) or router.METHOD(path, handler). The path can be a string, a string pattern with named parameters, or a regular expression. Named parameters are captured in req.params. Query string parameters are parsed into req.query. The express.Router() creates a modular, mountable route handler that behaves like a mini-application.
Why Express routing matters
The matching problem. The raw http module gives a handler for every request and leaves the matching to the developer. A real application has dozens or hundreds of endpoints. Express provides a routing table that matches method and path, extracts parameters, and calls the correct handler. The matching is ordered and predictable: routes are evaluated in the order they are defined, and the first match wins.
The parameter problem. URLs carry data. A user ID in the path, a search term in the query string, a filter in the fragment. Express parses these automatically. The path parameters are in req.params, and the query string is in req.query. The handler does not need to parse the URL manually.
The organization problem. A large application has routes for users, products, orders, and more. Putting them all in one file is unmaintainable. The express.Router() object creates a modular route handler that can be mounted at a path prefix. Each module owns its routes, and the main application mounts the modules.
The middleware problem. Routes can have route-specific middleware. A route can require authentication, validate input, or log the request before the handler runs. Middleware functions are passed between the path and the handler, and they run in order.
The versioning problem. APIs change. A route can be mounted at /api/v1 and another at /api/v2. The Router object makes versioning straightforward: each version is a router mounted at a different prefix.
a. Route methods and path patterns
Express provides a method for each HTTP verb. The method name is lowercase and matches the verb.
const express = require('express');
const app = express();
app.get('/users', (req, res) => res.send('GET users'));
app.post('/users', (req, res) => res.send('POST users'));
app.put('/users', (req, res) => res.send('PUT users'));
app.patch('/users', (req, res) => res.send('PATCH users'));
app.delete('/users', (req, res) => res.send('DELETE users'));
app.options('/users', (req, res) => res.send('OPTIONS users'));
app.head('/users', (req, res) => res.end());
The app.all() method matches every HTTP method for a path. It is useful for middleware that should run regardless of the method.
app.all('/secret', (req, res, next) => {
console.log('Accessing the secret section');
next();
});
The app.use() method is different. It matches a path prefix rather than an exact path, and it accepts middleware functions. A route defined with app.get() matches only GET requests to the exact path.
Path patterns support several forms. A plain string matches the exact path.
app.get('/about', (req, res) => res.send('About'));
A string with named parameters matches any value in that position.
app.get('/users/:userId', (req, res) => {
res.send(`User ${req.params.userId}`);
});
A string with optional parameters uses a ? suffix.
app.get('/users/:userId?', (req, res) => {
res.send(req.params.userId ? `User ${req.params.userId}` : 'All users');
});
A wildcard matches any suffix.
app.get('/files/*', (req, res) => {
res.send(`File path: ${req.params[0]}`);
});
A regular expression matches any path that satisfies the pattern.
app.get(/.*fly$/, (req, res) => {
res.send('Matches paths ending in fly');
});
Path patterns can be combined. A route can have multiple parameters, optional segments, and wildcards. The matching is left to right, and the first matching route wins.
Route paths are case-insensitive by default. The case sensitive routing setting changes this.
app.set('case sensitive routing', true);
With case-sensitive routing, /Users and /users are different routes.
b. Route parameters and query strings
Route parameters are named segments in the path. They are prefixed with a colon and are captured into req.params.
app.get('/users/:userId/books/:bookId', (req, res) => {
res.send(req.params);
// { userId: '42', bookId: '7' }
});
The parameter names are the keys in req.params. The values are strings. If the value represents a number, it must be parsed.
app.get('/users/:userId', (req, res) => {
const userId = parseInt(req.params.userId, 10);
if (isNaN(userId)) {
return res.status(400).send('Invalid user ID');
}
res.send(`User ${userId}`);
});
The req.params object is not the same as req.query. The parameters come from the path; the query comes from the URL’s query string.
app.get('/search', (req, res) => {
res.send(req.query);
});
A request to /search?q=express&limit=10&sort=asc sets req.query to { q: 'express', limit: '10', sort: 'asc' }. The values are strings. The URLSearchParams object is not used directly; Express parses the query string into a plain object.
The req.query object can contain nested objects and arrays when the query string uses bracket notation.
// GET /search?filter[status]=active&tags[]=js&tags[]=node
app.get('/search', (req, res) => {
res.send(req.query);
// { filter: { status: 'active' }, tags: ['js', 'node'] }
});
This is the qs library’s default behavior, which Express uses. The nesting is convenient but can be surprising. The query parser setting can be changed to simple to disable nesting.
app.set('query parser', 'simple');
The req.query is always an object. If the query string is empty, it is {}. If a parameter appears multiple times, the value is an array.
The req.params and req.query are read-only from the handler’s perspective. They are populated by Express before the handler runs. They can be modified, but the modifications do not affect the URL.
The req.baseUrl and req.originalUrl properties provide the mount path and the original URL. They are useful when a router is mounted at a prefix.
app.use('/api', router);
// In a route inside the router:
app.get('/users', (req, res) => {
console.log(req.baseUrl); // '/api'
console.log(req.originalUrl); // '/api/users'
console.log(req.path); // '/users'
});
The req.path is the path within the router. The req.baseUrl is the mount prefix. The req.originalUrl is the full URL as it arrived.
c. The Router object and modular routes
The express.Router() function creates a router. A router is a complete middleware and routing system. It can be mounted at a path prefix in the main application.
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.send('User list'));
router.get('/:id', (req, res) => res.send(`User ${req.params.id}`));
router.post('/', (req, res) => res.send('Create user'));
module.exports = router;
The router is exported and mounted in the main application.
const express = require('express');
const app = express();
const usersRouter = require('./routes/users');
app.use('/users', usersRouter);
A request to /users/ is handled by the router’s / route. A request to /users/42 is handled by the router’s /:id route. The /users prefix is stripped before the router matches, and req.baseUrl is set to /users.
Routers can be nested. A router can mount another router.
const usersRouter = express.Router();
const booksRouter = express.Router();
booksRouter.get('/', (req, res) => res.send('Book list'));
booksRouter.get('/:bookId', (req, res) => res.send(`Book ${req.params.bookId}`));
usersRouter.use('/:userId/books', booksRouter);
app.use('/users', usersRouter);
A request to /users/42/books/7 is handled by the nested router. The req.params contains both userId and bookId because the parameters are merged.
Router-level middleware applies only to the routes in that router.
router.use((req, res, next) => {
console.log('Router middleware');
next();
});
The middleware runs for every request that the router handles. It does not run for requests that the router does not match.
Route-specific middleware runs before a specific route’s handler.
function requireAuth(req, res, next) {
if (!req.headers.authorization) {
return res.status(401).send('Unauthorized');
}
next();
}
router.get('/admin', requireAuth, (req, res) => {
res.send('Admin page');
});
The requireAuth middleware runs only for the /admin route. If it does not call next(), the handler does not run.
Multiple handlers can be passed to a route. They run in order.
router.get('/protected',
requireAuth,
validateInput,
(req, res) => {
res.send('Protected data');
}
);
The route has three handlers: requireAuth, validateInput, and the final handler. Each calls next() to pass control to the next. The final handler sends the response.
Complete Example Session
// ============================================
// PART 1: BASIC ROUTES
// ============================================
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Home'));
app.get('/about', (req, res) => res.send('About'));
app.get('/contact', (req, res) => res.send('Contact'));
// ============================================
// PART 2: HTTP METHOD ROUTES
// ============================================
app.get('/users', (req, res) => res.send('GET users'));
app.post('/users', (req, res) => res.send('POST users'));
app.put('/users/:id', (req, res) => res.send(`PUT user ${req.params.id}`));
app.delete('/users/:id', (req, res) => res.send(`DELETE user ${req.params.id}`));
// ============================================
// PART 3: ROUTE PARAMETERS
// ============================================
app.get('/users/:userId/books/:bookId', (req, res) => {
res.send(req.params);
// { userId: '42', bookId: '7' }
});
// ============================================
// PART 4: OPTIONAL PARAMETERS
// ============================================
app.get('/users/:userId?', (req, res) => {
if (req.params.userId) {
res.send(`User ${req.params.userId}`);
} else {
res.send('All users');
}
});
// ============================================
// PART 5: QUERY STRING
// ============================================
app.get('/search', (req, res) => {
const { q, limit, sort } = req.query;
res.send(`Searching for ${q}, limit ${limit}, sort ${sort}`);
});
// ============================================
// PART 6: NESTED QUERY PARAMETERS
// ============================================
app.get('/filter', (req, res) => {
res.send(req.query);
// /filter?status=active&tags[]=js&tags[]=node
// { status: 'active', tags: ['js', 'node'] }
});
// ============================================
// PART 7: ROUTER MODULE
// ============================================
const router = express.Router();
router.get('/', (req, res) => res.send('User list'));
router.get('/:id', (req, res) => res.send(`User ${req.params.id}`));
module.exports = router;
// ============================================
// PART 8: MOUNTING A ROUTER
// ============================================
const usersRouter = require('./routes/users');
app.use('/users', usersRouter);
// ============================================
// PART 9: NESTED ROUTERS
// ============================================
const booksRouter = express.Router({ mergeParams: true });
booksRouter.get('/', (req, res) => {
res.send(`Books for user ${req.params.userId}`);
});
usersRouter.use('/:userId/books', booksRouter);
// ============================================
// PART 10: ROUTE-SPECIFIC MIDDLEWARE
// ============================================
function requireAuth(req, res, next) {
if (!req.headers.authorization) {
return res.status(401).send('Unauthorized');
}
next();
}
router.get('/admin', requireAuth, (req, res) => {
res.send('Admin page');
});
The ten parts covered basic routes, method routes, route parameters, optional parameters, query strings, nested query parameters, the Router module, mounting a router, nested routers, and route-specific middleware.
Quick Reference
Route Methods
| Method | Verb |
|---|---|
app.get() | GET |
app.post() | POST |
app.put() | PUT |
app.patch() | PATCH |
app.delete() | DELETE |
app.options() | OPTIONS |
app.head() | HEAD |
app.all() | All methods |
Path Patterns
| Pattern | Matches |
|---|---|
/users | Exact path |
/users/:id | Named parameter |
/users/:id? | Optional parameter |
/files/* | Wildcard |
/.*fly$/ | Regular expression |
Request Properties
| Property | Value |
|---|---|
req.params | Path parameters |
req.query | Query string parameters |
req.body | Parsed body |
req.path | Path within router |
req.baseUrl | Mount prefix |
req.originalUrl | Full URL |
Router Methods
| Method | Purpose |
|---|---|
express.Router() | Create router |
router.get() | Define GET route |
router.post() | Define POST route |
router.use() | Middleware or sub-router |
app.use('/path', router) | Mount router |
express.Router({ mergeParams: true }) | Merge parent params |
Best Practices
✅ Do This:
// Use express.Router() for modular routes
const router = express.Router(); // ✅
// Mount routers at path prefixes
app.use('/users', usersRouter); // ✅
// Parse route parameters explicitly
const id = parseInt(req.params.id, 10); // ✅
// Use mergeParams for nested routers
express.Router({ mergeParams: true }); // ✅
// Validate parameters before use
if (isNaN(id)) return res.status(400).send('Invalid ID'); // ✅
// Use route-specific middleware for auth
router.get('/admin', requireAuth, handler); // ✅
// Set case-sensitive routing if needed
app.set('case sensitive routing', true); // ✅
❌ Don’t Do This:
// Don't put all routes in one file
app.get('/users', ...);
app.get('/products', ...);
// ... 50 more // ⚠️
// Don't assume req.params values are numbers
const id = req.params.id; // string // ⚠️
// Don't forget mergeParams for nested routers
const router = express.Router(); // parent params lost // ❌
// Don't use app.use() when you mean app.get()
app.use('/users', handler); // matches all methods and prefixes // ⚠️
// Don't define routes after the error handler
app.use(errorHandler);
app.get('/route', handler); // never reached // ❌
// Don't rely on query parameter types
const limit = req.query.limit; // string // ⚠️
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Route not matched | Wrong method or path | Check method and path |
req.params empty | Parameter not in path | Add :param |
| Nested router params lost | No mergeParams | express.Router({ mergeParams: true }) |
| Query values are strings | HTTP is text | Parse with parseInt |
| Route order matters | First match wins | Define specific before general |
app.use vs app.get | use matches prefixes | Use get for exact routes |
| Error handler not called | Defined before routes | Move to the end |
Real-World Examples
1. REST API
router.get('/users', listUsers);
router.post('/users', createUser);
router.get('/users/:id', getUser);
router.put('/users/:id', updateUser);
router.delete('/users/:id', deleteUser);
2. Versioned API
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
3. Search Endpoint
app.get('/search', (req, res) => {
const { q, page = 1, limit = 10 } = req.query;
res.json({ query: q, page: Number(page), limit: Number(limit) });
});
4. Filter with Array
// /products?tags[]=sale&tags[]=new
app.get('/products', (req, res) => {
const tags = req.query.tags || [];
res.json({ tags });
});
5. Nested Resource
usersRouter.use('/:userId/posts', postsRouter);
6. Auth Middleware
function requireAuth(req, res, next) {
if (!req.session.user) return res.status(401).send('Login required');
next();
}
app.get('/dashboard', requireAuth, dashboardHandler);
7. Optional Parameter
app.get('/posts/:year?/:month?', (req, res) => {
res.send(req.params);
});
8. Wildcard Route
app.get('/files/*', (req, res) => {
res.send(`Path: ${req.params[0]}`);
});
9. Regex Route
app.get(/.*\.json$/, (req, res) => {
res.json({ type: 'json' });
});
10. Multiple Handlers
app.get('/protected',
requireAuth,
logAccess,
(req, res) => res.send('Data')
);
Visual
Route Matching Flow
┌─────────────────────────────────────────────────────────────┐
│ REQUEST: GET /users/42 │
│ │ │
│ ▼ │
│ Route 1: app.get('/') → no match │
│ │ │
│ ▼ │
│ Route 2: app.get('/about') → no match │
│ │ │
│ ▼ │
│ Route 3: app.get('/users/:id') → MATCH │
│ │ │
│ ▼ │
│ req.params = { id: '42' } │
│ │ │
│ ▼ │
│ Handler runs │
│ │
│ First matching route wins. Order matters. │
│ │
└─────────────────────────────────────────────────────────────┘
Path Parameter vs Query String
┌─────────────────────────────────────────────────────────────┐
│ URL: /users/42?active=true&sort=name │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ /users/42 │ │
│ │ └── req.params.id = '42' │ │
│ │ │ │
│ │ ?active=true&sort=name │ │
│ │ └── req.query.active = 'true' │ │
│ │ └── req.query.sort = 'name' │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Path parameters identify a resource. │
│ Query parameters modify or filter the response. │
│ │
└─────────────────────────────────────────────────────────────┘
Router Mounting
┌─────────────────────────────────────────────────────────────┐
│ MAIN APP │
│ app.use('/users', usersRouter) │
│ app.use('/products', productsRouter) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ usersRouter │ │
│ │ GET / → /users │ │
│ │ GET /:id → /users/:id │ │
│ │ POST / → /users │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ productsRouter │ │
│ │ GET / → /products │ │
│ │ GET /:id → /products/:id │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ The router's paths are relative to the mount point. │
│ │
└─────────────────────────────────────────────────────────────┘
Nested Router Parameters
┌─────────────────────────────────────────────────────────────┐
│ usersRouter.use('/:userId/books', booksRouter) │
│ │
│ REQUEST: GET /users/42/books/7 │
│ │ │
│ ▼ │
│ usersRouter matches /:userId │
│ req.params = { userId: '42' } │
│ │ │
│ ▼ │
│ booksRouter mounted at /:userId/books │
│ (with mergeParams: true) │
│ │ │
│ ▼ │
│ booksRouter matches /:bookId │
│ req.params = { userId: '42', bookId: '7' } │
│ │
│ Without mergeParams, userId is lost. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Route definition | app.METHOD(path, handler) |
| All methods | app.all(path, handler) |
| Path parameter | :name |
| Optional parameter | :name? |
| Wildcard | * |
| Regex | /pattern/ |
| Path parameters | req.params |
| Query parameters | req.query |
| Router | express.Router() |
| Mount router | app.use('/prefix', router) |
| Nested params | express.Router({ mergeParams: true }) |
| Route middleware | app.get(path, mw, handler) |
Key takeaways:
- Routes are matched in order, and the first match wins. The order of route definitions matters. Specific routes should be defined before general ones.
- Path parameters are named segments prefixed with
:. They are captured intoreq.params. The values are strings and must be parsed if they represent numbers. - Query string parameters are parsed into
req.query. They are also strings. Nested objects and arrays are supported through bracket notation. req.paramscomes from the path;req.querycomes from the query string. The two are separate and serve different purposes. Path parameters identify a resource; query parameters modify or filter.- The
express.Router()creates a modular route handler. It is mounted at a path prefix withapp.use(). The router’s paths are relative to the mount point.req.baseUrlis the mount prefix. - Nested routers require
mergeParamsto access parent parameters. Without it, the parent’s parameters are lost when the child router matches. - Route-specific middleware runs before the handler. Multiple handlers can be passed to a route, and they run in order. Each calls
next()to pass control to the next.
Remember: Routing is the core of an Express application. The method and path determine which handler runs. The path parameters and query string carry the data. The router organizes the routes into modules. The middleware applies cross-cutting concerns. Get the routing right, and the application is predictable. Get it wrong—wrong method, wrong path, wrong order—and requests fail with 404s or run the wrong handler. The rules are simple: first match wins, params are strings, query is separate from path, and routers are mounted at prefixes. Learn the rules, and the routing table becomes a map of the application.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!