Routing¶
Routes are just if statements with better PR. Mess them up and you're routing traffic to the wrong handler
Every request hits your router , gets matched against patterns , and lands in a handler. If your patterns are too loose you catch requests you shouldn't. Too tight and you 404 legitimate traffic. Parameter pollution , path traversal , regex denial of service - your router is attack surface whether you treat it like one or not
HTTP method routing¶
Express maps methods to functions:
app.get('/resource' , handler) // READ
app.post('/resource' , handler) // CREATE
app.put('/resource/:id' , handler) // FULL UPDATE
app.patch('/resource/:id' , handler)// PARTIAL UPDATE
app.delete('/resource/:id' , handler)// DELETE
app.all('/resource' , handler) // matches ANY method
app.all() is useful for middleware that needs to run regardless of method:
app.all('/api/*' , (req , res , next) => {
// runs for GET , POST , PUT , DELETE on /api/*
console.log(`${req.method} ${req.path}`)
next()
})
route params - :id style¶
Named parameters in URL paths:
app.get('/users/:userId/orders/:orderId' , (req , res) => {
// GET /users/42/orders/7
// req.params.userId -> '42'
// req.params.orderId -> '7'
res.json(req.params)
})
Route params are strings. Always. Parse them to numbers if you need math
app.get('/users/:id' , (req , res) => {
const id = parseInt(req.params.id , 10)
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid user ID' })
}
// proceed with numeric id
})
query strings¶
Express parses URL query strings into req.query automatically
// GET /users?role=admin&active=true&page=2
app.get('/users' , (req , res) => {
// req.query.role -> 'admin'
// req.query.active -> 'true'
// req.query.page -> '2'
res.json(req.query)
})
Query params are also strings. req.query.active is the string 'true' , not boolean true
const active = req.query.active === 'true'
route ordering matters¶
Express matches routes in the order they're defined. First match wins
// BAD - catch-all before specific route
app.get('/users/:id' , handler) // this catches everything
app.get('/users/me' , handler) // NEVER REACHED - /users/me matches :id
// GOOD - specific routes first
app.get('/users/me' , handler) // matched first
app.get('/users/:id' , handler) // only matches actual IDs
This seems obvious until you're debugging why /users/profile returns user data for ID "profile"
express.Router() - modular routes¶
Don't cram every route into app.js. Use Router for modular , mountable route groups
// src/routes/users.js
const express = require('express')
const router = express.Router()
// middleware specific to this router
router.use((req , res , next) => {
console.log('Users router accessed at:', Date.now())
next()
})
router.get('/' , (req , res) => {
res.json({ users: [] })
})
router.get('/:id' , (req , res) => {
res.json({ userId: req.params.id })
})
router.post('/' , (req , res) => {
res.status(201).json({ created: true })
})
module.exports = router
Mount it in your main app:
// src/app.js
const userRoutes = require('./routes/users')
app.use('/users' , userRoutes)
// Now:
// GET /users -> router.get('/')
// GET /users/:id -> router.get('/:id')
// POST /users -> router.post('/')
route-level middleware¶
Middleware can be applied to specific routes:
const { authenticate } = require('../middleware/auth')
// public
router.get('/public' , handler)
// protected - authenticate runs before the handler
router.get('/profile' , authenticate , handler)
// multiple middleware
router.post('/' , validateBody , sanitizeInput , handler)
wildcard and 404 handler¶
Catch-all route for undefined paths:
// must be LAST route defined
app.use((req , res) => {
res.status(404).json({ error: 'Not found' })
})
This is critical - if you don't define a 404 handler , Express's default handler sends a bare "Cannot GET /path" message. That leaks your framework and path structure
regex in routes¶
Express supports regex patterns in route paths (use with caution):
// match /user , /users , /userssss
app.get(/\/user(s)?/ , handler)
// match routes with numeric IDs only
app.get('/users/:id(\\d+)' , handler)
Regex denial of service is real. Complex regex patterns in routes can be exploited with crafted inputs that cause catastrophic backtracking. Keep route regex simple or use explicit validation in the handler instead
route security¶
// parameter pollution - what if someone sends /users?id=1&id=2&id=3
app.get('/users' , (req , res) => {
// req.query.id could be string or array
const ids = Array.isArray(req.query.id)
? req.query.id.map(i => parseInt(i , 10))
: [parseInt(req.query.id , 10)]
if (ids.some(isNaN)) {
return res.status(400).json({ error: 'Invalid IDs' })
}
// proceed with array of IDs
})
Express parses ?id=1&id=2 as an array ['1' , '2']. If your code expects a single value you get undefined behavior or crashes. Always validate , always type-check
prerequisites¶
express_02_get_started.md - project structure , app.js , Nodemon
next → express_04_middleware.md