Skip to content

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