Routing Basics

beginner express routing http

A route in Express is a mapping from (HTTP method, URL path) to a handler function. When a request comes in, Express finds the first matching route and runs its handler.

In simple language — “when someone does a GET on /users, run this function.” That’s it.

The shape

app.METHOD(PATH, HANDLER);
  • METHOD — http verb in lowercase: get, post, put, delete, patch
  • PATH — URL pattern: /users, /users/:id, /api/*
  • HANDLER(req, res) => { ... }
const express = require('express');
const app = express();

app.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'Manish' }]);
});

app.post('/users', (req, res) => {
  res.status(201).json({ id: 2, name: 'New User' });
});

app.put('/users/:id', (req, res) => {
  res.json({ id: req.params.id, updated: true });
});

app.delete('/users/:id', (req, res) => {
  res.status(204).end();
});

app.listen(3000);

Sending responses

The res object has helpers for every common case. You only need to call one of these per request — calling two throws “Cannot set headers after they are sent.”

res.send('Hello');              // auto-detects content type
res.json({ ok: true });         // sets Content-Type: application/json
res.status(404).json({ ... });  // chainable status code
res.sendStatus(204);            // sets status + sends status text
res.redirect('/login');         // 302 redirect
res.redirect(301, '/new');      // permanent redirect
res.end();                      // no body, just close

Status code defaults to 200. We set it explicitly for created (201), no content (204), errors (4xx/5xx).

Request Lifecycle
Client
GET /users/42
Express matches
route
Handler runs
res.json(...)
Response sent

Order matters

Express checks routes top to bottom. The first one that matches wins. So a generic catch-all should always go last.

app.get('/users/me', (req, res) => res.json({ self: true }));
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }));

// If we flipped these, /users/me would match :id first and return { id: 'me' }

Multiple handlers per route

We can pass multiple functions — they run in order, each calling next() to pass control:

const auth = (req, res, next) => {
  if (!req.headers.authorization) return res.sendStatus(401);
  next();
};

app.get('/profile', auth, (req, res) => {
  res.json({ name: 'Manish' });
});

This is just middleware — covered in detail later. Useful for per-route auth checks.

app.all() and route chaining

For all methods on a path, use app.all. To group methods on one path cleanly, use app.route:

app.route('/books')
  .get((req, res) => res.json([]))
  .post((req, res) => res.status(201).json({}))
  .put((req, res) => res.json({}));

This is mostly stylistic — same as writing three separate app.get/post/put calls.

What about async handlers?

Express 4 doesn’t auto-catch rejected promises. If we throw inside async (req, res), the request hangs forever:

// BROKEN in Express 4
app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id); // if this rejects, request hangs
  res.json(user);
});

Fix it with try/catch and next(err), or use a wrapper like express-async-errors. Express 5 handles this natively — but most production apps are still on v4.