Skip to content

Express Route Handlers

A route handler is the function (or functions) that ultimately respond to a matched request. This lesson covers the different ways Express lets you attach one or more handlers to a single route.

One or Many Handlers Per Route

The simplest route has a single handler function. But Express also accepts multiple handler functions, or an array of them, letting earlier ones act as route-specific middleware before the final one sends a response.

This is different from application- or router-level middleware because it's scoped to exactly one route definition, useful for logic that truly only applies there.

app.get(
  '/orders/:id',
  validateOrderId,      // runs first
  loadOrderOr404,        // runs second
  (req, res) => {         // final handler
    res.json(req.order);
  }
);

Each function calls next() to pass control to the next one in the list, exactly like any other middleware chain.

Route Handler Syntax Variants

app.get(path, handler);
app.get(path, mw1, handler);
app.get(path, mw1, mw2, handler);
app.get(path, [mw1, mw2, handler]); // array form
  • Any number of functions can be passed, each receiving (req, res, next).
  • The array form is functionally identical to listing them as separate arguments.
  • Only functions that call next() continue the chain, the last one is expected to respond.
  • Async handlers should still ultimately call a response method or next(err).

Route Handler Patterns Cheatsheet

Common ways to combine multiple functions into one route definition.

Pattern Example Use Case
Single handler app.get('/x', handler) Simple routes
Guard + handler app.get('/x', requireAuth, handler) Protecting a single route
Validate + handler app.post('/x', validate(schema), handler) Per-route validation
Array form app.get('/x', [mw1, mw2, handler]) Reusable handler lists
Async handler app.get('/x', asyncHandler(async (req,res)=>{...})) Database/API calls

Reusable Handler Arrays

When several routes need the exact same combination of middleware, defining that combination once as an array keeps route files short and consistent.

const protectedRoute = [requireAuth, requireVerifiedEmail];

app.get('/profile', ...protectedRoute, getProfile);
app.put('/profile', ...protectedRoute, updateProfile);

Async Route Handlers

Most real handlers need to await a database call or external API. Wrap the async logic so any rejected promise reaches your error-handling middleware instead of crashing silently.

const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/orders/:id', asyncHandler(async (req, res) => {
  const order = await Order.findById(req.params.id);
  if (!order) return res.status(404).json({ error: 'Not found' });
  res.json(order);
}));

Common Mistakes

  • Forgetting next() in a non-final handler, leaving the request hanging with no response.
  • Copy-pasting the same middleware combination across many routes instead of extracting a reusable array.
  • Letting an unhandled promise rejection inside an async handler crash the process instead of reaching the error handler.
  • Putting business logic directly in the route handler instead of delegating to a controller/service.

Key Takeaways

  • A route can have one handler or a chain of several, each with (req, res, next).
  • Route-scoped middleware (like a validation function) only applies to that specific route.
  • Reusable handler arrays reduce duplication across similar protected routes.
  • Wrap async handlers so rejected promises are forwarded to your error-handling middleware.

Pro Tip

Standardize one asyncHandler wrapper across your entire codebase early, retrofitting error handling into dozens of unguarded async routes later is far more work.