Routing
Mapping URLs to code - route parameters, query strings, and organizing your endpoints.
What is Routing?#
Routing is the process of matching URLs to code. When someone visits /users/123, routing figures out:
- Which function handles this request
- What parameters to extract (123 is the user ID)
// URL: GET /users/123
// Route matches and extracts id=123
app.get('/users/:id', (req, res) => {
const userId = req.params.id; // '123'
// ... fetch and return user
});
Basic Routes#
import express from 'express';
const app = express();
// HTTP method + path + handler
app.get('/users', getUsers); // GET /users
app.post('/users', createUser); // POST /users
app.get('/users/:id', getUser); // GET /users/123
app.patch('/users/:id', updateUser); // PATCH /users/123
app.delete('/users/:id', deleteUser); // DELETE /users/123
// All methods
app.all('/secret', authenticate);
Route Parameters#
Dynamic segments in URLs:
// Single parameter
app.get('/users/:id', (req, res) => {
console.log(req.params.id); // 'abc123'
});
// GET /users/abc123
// Multiple parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
console.log(req.params.userId); // '123'
console.log(req.params.postId); // '456'
});
// GET /users/123/posts/456
// Optional parameter (with regex)
app.get('/files/:filename.:ext?', (req, res) => {
console.log(req.params.filename); // 'report'
console.log(req.params.ext); // 'pdf' or undefined
});
// GET /files/report.pdf → filename='report', ext='pdf'
// GET /files/report → filename='report', ext=undefined
Query Strings#
Data in the URL after ?:
// GET /users?page=2&limit=20&sort=-createdAt
app.get('/users', (req, res) => {
console.log(req.query.page); // '2' (string!)
console.log(req.query.limit); // '20'
console.log(req.query.sort); // '-createdAt'
});
// Arrays
// GET /users?role=admin&role=moderator
console.log(req.query.role); // ['admin', 'moderator']
// Nested (with qs library)
// GET /users?filter[status]=active&filter[role]=admin
console.log(req.query.filter); // { status: 'active', role: 'admin' }
Query Params are Strings
req.query.page is '2' not 2. Always parse them:
const page = parseInt(req.query.page, 10) || 1;
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
Route Organization#
Single File (Small Apps)#
// src/app.js
import express from 'express';
const app = express();
app.get('/users', ...);
app.post('/users', ...);
app.get('/users/:id', ...);
app.get('/posts', ...);
app.post('/posts', ...);
// Gets messy fast...
Router Files (Recommended)#
// src/routes/users.js
import { Router } from 'express';
import * as userController from '../controllers/users.js';
const router = Router();
router.get('/', userController.list);
router.post('/', userController.create);
router.get('/:id', userController.get);
router.patch('/:id', userController.update);
router.delete('/:id', userController.remove);
export default router;
// src/routes/index.js
import { Router } from 'express';
import userRoutes from './users.js';
import postRoutes from './posts.js';
import authRoutes from './auth.js';
const router = Router();
router.use('/users', userRoutes); // /api/users/*
router.use('/posts', postRoutes); // /api/posts/*
router.use('/auth', authRoutes); // /api/auth/*
export default router;
// src/app.js
import express from 'express';
import routes from './routes/index.js';
const app = express();
app.use('/api', routes); // All routes prefixed with /api
Middleware in Routes#
Apply middleware to specific routes:
import { Router } from 'express';
import { authenticate } from '../middleware/auth.js';
import { validate } from '../middleware/validate.js';
import { createUserSchema, updateUserSchema } from '../validators/users.js';
const router = Router();
// Public routes - no auth required
router.get('/', userController.list);
router.get('/:id', userController.get);
// Protected routes - auth required
router.post('/',
authenticate, // Must be logged in
validate(createUserSchema), // Must send valid data
userController.create
);
router.patch('/:id',
authenticate,
validate(updateUserSchema),
userController.update
);
router.delete('/:id',
authenticate,
userController.remove
);
// Or apply to all routes in this router
router.use(authenticate); // Everything below requires auth
export default router;
Route Patterns#
RESTful Resources#
Standard CRUD operations:
GET /users → List all users
POST /users → Create user
GET /users/:id → Get one user
PATCH /users/:id → Update user
DELETE /users/:id → Delete user
Nested Resources#
Resources that belong to other resources:
// User's posts
router.get('/users/:userId/posts', getUserPosts);
router.post('/users/:userId/posts', createUserPost);
// Post's comments
router.get('/posts/:postId/comments', getPostComments);
router.post('/posts/:postId/comments', createComment);
Actions (Non-CRUD)#
Sometimes CRUD doesn't fit:
// Auth actions
router.post('/auth/login', login);
router.post('/auth/logout', logout);
router.post('/auth/refresh', refreshToken);
router.post('/auth/forgot-password', forgotPassword);
router.post('/auth/reset-password', resetPassword);
// Resource actions
router.post('/users/:id/verify', verifyUser);
router.post('/orders/:id/cancel', cancelOrder);
router.post('/posts/:id/publish', publishPost);
Route Order Matters#
Express matches routes in order:
// WRONG - :id matches 'profile' as a parameter
router.get('/:id', getUser); // Matches /profile → id='profile'
router.get('/profile', getProfile); // Never reached!
// RIGHT - specific routes first
router.get('/profile', getProfile); // Matches /profile
router.get('/:id', getUser); // Matches /123
Request Body#
For POST, PUT, PATCH requests:
app.use(express.json()); // Parse JSON bodies
app.post('/users', (req, res) => {
console.log(req.body); // { name: 'John', email: 'john@example.com' }
});
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"john@example.com"}'
Response Methods#
// JSON response (most common for APIs)
res.json({ data: users });
// Status + JSON
res.status(201).json({ data: newUser });
res.status(404).json({ error: 'Not found' });
// No content
res.status(204).send();
// Redirect
res.redirect('/new-location');
res.redirect(301, '/permanent-new-location');
// Send file
res.sendFile('/path/to/file.pdf');
// Set headers
res.set('X-Custom-Header', 'value');
res.json({ data });
Error Handling in Routes#
Let errors bubble up to error middleware:
// Route handler - throw errors
router.get('/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('User'); // Will be caught by error middleware
}
res.json({ data: user });
});
// Error middleware - catches all errors
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
res.status(status).json({
error: {
message: err.message,
code: err.code || 'INTERNAL_ERROR',
},
});
});
Route Parameter Validation#
Validate parameters before the handler runs:
// Middleware to validate MongoDB ObjectId
function validateObjectId(paramName) {
return (req, res, next) => {
const id = req.params[paramName];
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new BadRequestError(`Invalid ${paramName}`);
}
next();
};
}
// Use it
router.get('/:id', validateObjectId('id'), userController.get);
Or use router.param():
router.param('id', (req, res, next, id) => {
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new BadRequestError('Invalid ID');
}
next();
});
// Now all routes with :id are validated
router.get('/:id', getUser);
router.patch('/:id', updateUser);
router.delete('/:id', deleteUser);
Route Naming Conventions#
// Use nouns, not verbs
GET /users ✓
GET /getUsers ✗
// Plural for collections
GET /users ✓
GET /user ✗
// Lowercase, hyphenated
GET /user-profiles ✓
GET /userProfiles ✗
GET /user_profiles ✗
// No trailing slashes
GET /users ✓
GET /users/ ✗
Key Takeaways#
- Organize routes in separate files - One file per resource
- Use Router() - Modular, composable routing
- Order matters - Specific routes before parameterized routes
- Parse query params - They're strings, convert them
- Apply middleware selectively - Only where needed
- Follow REST conventions - Predictable API design
The Pattern
routes/index.js → Combines all route files
routes/users.js → /users/* routes
routes/posts.js → /posts/* routes
controllers/*.js → Handler functions
middleware/*.js → Auth, validation, etc.
Keep routes thin. They define URLs and wire up middleware. Logic goes in controllers and services.
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.