Skip to main content

API handler pattern

When writing an HTTP API, the code around the handler body (auth, validation, logging, error translation, rate limiting) often outweighs the body itself. Where and how this cross-cutting code lives decides consistency a…

3 viewsAbout 4 min read
Table of contents

When writing an HTTP API, the code around the handler body (auth, validation, logging, error translation, rate limiting) often outweighs the body itself. Where and how this cross-cutting code lives decides consistency and maintenance cost.

1. Per-framework mechanisms

The mechanisms for expressing cross-cutting concerns vary in name across frameworks.

Framework Mechanism Form
Express (Node) middleware (req, res, next) => ...
Koa middleware async (ctx, next) => ...
Spring MVC HandlerInterceptor · Filter Class + annotation
FastAPI Depends · middleware Function dependency + ASGI middleware
NestJS Interceptor · Guard · Pipe Decorator + class
Next.js Route Handlers Wrapper function withAuth(handler) style composition
Hono middleware app.use(...)

One standard response format is RFC 7807 — Problem Details for HTTP APIs (2016, later revised as RFC 9457). It defines the application/problem+json content type and the type · title · status · detail · instance fields.

2. Middleware chain (Express model)

A request passes through middlewares in the order they were registered, and each calls next() to advance. Sending a response directly or throwing an error breaks the chain there.

app.use(requestId)
app.use(logger)
app.use(authenticate)
app.use('/admin', authorize('admin'))
app.use(routes)
app.use(errorHandler)  // (err, req, res, next) — error-only

Koa re-expressed the same model in async/await. It is called the "onion model" because the same middleware can wake up after the response to do final processing.

3. Spring's Interceptor and Filter

A Servlet Filter sits at the outermost layer; HandlerInterceptor sits closer to the controller. @RestControllerAdvice translates exceptions in one place.

@RestControllerAdvice
class GlobalExceptionHandler {
  @ExceptionHandler(NotFoundException.class)
  ResponseEntity<ProblemDetail> notFound(NotFoundException e) {
    var p = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
    return ResponseEntity.of(p).build();
  }
}

From Spring 6, the ProblemDetail class produces RFC 7807 responses in a standardized way.

4. FastAPI's Depends + middleware

Depends is evaluated right before the handler enters and carries auth, DB session, and validation. app.add_middleware carries ASGI-level global processing. Beyond HTTPException, FastAPI can translate user-defined exceptions via app.exception_handler.

5. Next.js wrapper

Next.js App Router's route.ts is a function. Cross-cutting concerns are added by composition.

export const GET = withAuth(withLogging(async (req) => {
  return NextResponse.json({ ok: true })
}))

Function composition does not use decorators or classes — it stays light, but order and types must be managed by hand.

6. RFC 7807 response

{
  "type": "https://example.com/probs/out-of-credit",
  "title": "You do not have enough credit.",
  "status": 403,
  "detail": "Your current balance is 30, but that costs 50.",
  "instance": "/account/12345/msgs/abc"
}

The strength is that an API client can assume standard fields when parsing the response. Adding custom fields like errors[] is also allowed by the spec.

7. Function composition vs class decorators

  • Function composition (Express · Hono · Next wrapper) — the flow reads top-to-bottom. Dependencies are explicit. Indentation grows as composition deepens.
  • Class decorators (NestJS · Spring) — declared via metadata. Handlers stay short. Annotation order and priority can be implicit.
  • Dependency injection (FastAPI Depends) — dependencies are written in the signature. A middle ground between composition and decorators.

Whichever form, the goal is the same — cross-cutting concerns gather in one place and the handler focuses on the body.

8. ApiError standard response class

Having a custom ApiError class is a common pattern.

class ApiError extends Error {
  constructor(public status: number, public code: string, public detail?: string) { super(detail) }
}

When thrown, a global handler translates it into problem+json. Mapping RFC 7807's type to a domain code lets clients branch on it.

9. Middleware order

The following order is generally safe.

① Request ID assignment (observability)
② Security headers (Helmet-style)
③ CORS
④ Logging
⑤ Body parsing
⑥ Authentication
⑦ Authorization (per route)
⑧ Validation (schema)
⑨ Route handler
⑩ Error handler (last)

Rate limiting belongs at the middleware layer. Redis-backed token bucket and sliding window are common (express-rate-limit · @upstash/ratelimit · Spring's Bucket4j).

10. Common pitfalls

Error middleware not catching async throws — Express 4 does not auto-propagate rejections from async handlers. A helper like express-async-errors or a try/catch wrapper is needed. Express 5 improved this.

Sending the response twice — if one middleware sends a response and another sends again, ERR_HTTP_HEADERS_SENT is raised. Tighten the timing of next() calls and returns.

Client IP and proxies — when behind Caddy or nginx, missing X-Forwarded-For trust configuration causes rate limiting to lump everything into the proxy IP.

Information leaks in error messages — make sure stack traces and DB messages are not put into detail directly, and mask in production.

Closing thoughts

Where cross-cutting concerns become half of the code, the value of bundling via wrappers or middleware shows up clearly. Starting with direct try/catch and moving to abstractions when patterns repeat is the safest flow.

Next

  • jobs-apscheduler
  • typeorm-readonly

See RFC 7807 · RFC 9457 (revision) · Express middleware · Spring RestControllerAdvice · FastAPI Dependencies · NestJS Interceptors · Hono Middleware.

More in backend

All in this category →

Related posts

Was this article helpful?