Every snippet on this page is copied from examples/ in this repo, and every example cited has a matching file in test/acceptance/ that runs it against real requests -- so these patterns are continuously verified, not documentation folklore. Run any of them directly: node examples/error.

Centralized error handling

One arity-4 middleware, registered after all routes, catches both thrown errors and next(err):

examples/error/index.js
function error(err, req, res, next) {
  // log it
  if (!test) console.error(err.stack);

  // respond with 500 "Internal Server Error".
  res.status(500);
  res.send('Internal Server Error');
}

app.get('/', function () {
  // Caught and passed down to the errorHandler middleware
  throw new Error('something broke!');
});

app.get('/next', function(req, res, next){
  // We can also pass exceptions to next()
  // …
  process.nextTick(function(){
    next(new Error('oh no!'));
  });
});

// the error handler is placed after routes
// if it were above it would not receive errors
// from app.get() etc
app.use(error);

The placement comment is the rule to remember. For distinct 404 versus error pages, examples/error-pages/index.js extends this with two final layers: a plain middleware that renders 404 (reached only when nothing matched), followed by an arity-4 handler that reads err.status.

File downloads with error recovery

res.download takes a callback, which is the only way to distinguish "file missing" from "transfer failed halfway":

examples/downloads/index.js
// /files/* is accessed via req.params[0]
// but here we name it :file
app.get('/files/*file', function (req, res, next) {
  res.download(req.params.file.join('/'), { root: FILES_DIR }, function (err) {
    if (!err) return; // file sent
    if (err.status !== 404) return next(err); // non-404 error
    // file for download not found
    res.statusCode = 404;
    res.send('Cant find that file, sorry!');
  });
});

Two Express 5 details here: splat params are named (*file), and a splat's value arrives as an array of path segments -- hence the .join('/'). The root option confines resolution to FILES_DIR, which is what makes serving a user-supplied path safe.

Validating route parameters once

app.param centralizes conversion and existence checks so handlers receive clean values or never run:

examples/params/index.js
// Load user by id

app.param('user', function(req, res, next, id){
  req.user = users[id]
  if (req.user) {
    next();
  } else {
    next(createError(404, 'failed to find user'));
  }
});
// …
app.get('/user/:user', function (req, res) {
  res.send('user ' + req.user.name);
});

createError from http-errors attaches the status so the error handler (or finalhandler) responds 404, not 500. The dispatch guarantee that makes this pattern viable -- the callback runs once per request even across nested routers -- is pinned in test/Router.js.

Splitting an app into routers

Version-prefixed APIs as separate router modules:

examples/multi-router/index.js
app.use('/api/v1', require('./controllers/api_v1'));
app.use('/api/v2', require('./controllers/api_v2'));

Each controller file builds and exports its own router, written as if mounted at /:

examples/multi-router/controllers/api_v1.js
var apiv1 = express.Router();

apiv1.get('/', function(req, res) {
  res.send('Hello from APIv1 root route.');
});

apiv1.get('/users', function(req, res) {
  res.send('List of APIv1 users.');
});

module.exports = apiv1;

For larger apps, examples/route-separation keeps one flat route table in index.js pointing at handler modules (app.get('/users', user.list)), and examples/mvc goes fully convention-based, auto-mounting controllers/<name>/index.js. All three shapes compose, because routers nest.

One URL, three representations

res.format selects a handler by the request's Accept header, falling back to the first key when no Accept is present:

examples/content-negotiation/index.js
app.get('/', function(req, res){
  res.format({
    html: function(){
      res.send('<ul>' + users.map(function(user){
        return '<li>' + user.name + '</li>';
      }).join('') + '</ul>');
    },

    text: function(){
      res.send(users.map(function(user){
        return ' - ' + user.name + '\n';
      }).join(''));
    },

    json: function(){
      res.json(users);
    }
  });
});

Unmatched types get a 406 with the offered types listed, and Vary: Accept is set automatically. The same example file goes on to wrap this in a tiny middleware factory (format('./users')) for a declarative per-resource layout.

Remembering state with cookies

examples/cookies/index.js shows the full write-read-clear cycle: express.urlencoded() parses the form, res.cookie('remember', 1, { maxAge: minute }) sets it, cookie-parser middleware populates req.cookies.remember on later requests, and res.clearCookie('remember') forgets it. Session-backed variants live in examples/session (server-side store) and examples/cookie-sessions (cookie-payload sessions).

Sources: examples/error/index.js, examples/downloads/index.js, examples/params/index.js, examples/multi-router/index.js, examples/content-negotiation/index.js · last synced 2026-07-27 · a371447 · version 5.2.1