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):
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":
// /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:
// 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:
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 /:
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:
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).