Middleware

Middleware are async functions that run before your route handlers. They receive the same ctx plus a next function. Call await next() to continue to the next middleware (or the route handler). Don't call it to stop the request early — for example, to reject an unauthorized request.

Every middleware has the signature: (ctx: HttpContext, next: NextFunction) => Promise<void>

Writing middleware

A simple request logger — runs after the handler returns so it can log the duration:

src/middleware/LoggerMiddleware.ts
import type { HttpContext, NextFunction } from '@pearl-framework/pearl'

export async function LoggerMiddleware(ctx: HttpContext, next: NextFunction) {
  const start = Date.now()
  await next()  // run the handler
  const ms = Date.now() - start
  console.log(`${ctx.request.method} ${ctx.request.url} ${ms}ms`)
}

Short-circuiting (blocking requests)

Return early without calling next() to stop the request. The handler and any subsequent middleware won't run:

src/middleware/RequireApiKey.ts
import type { HttpContext, NextFunction } from '@pearl-framework/pearl'

export async function RequireApiKey(ctx: HttpContext, next: NextFunction) {
  const key = ctx.request.headers.get('x-api-key')
  if (key !== process.env.API_KEY) {
    // Stop here — don't call next()
    ctx.response.status(401).json({ error: 'Invalid or missing API key' })
    return
  }
  await next()
}

CORS middleware

Handle CORS headers and preflight OPTIONS requests:

src/middleware/CorsMiddleware.ts
import type { HttpContext, NextFunction } from '@pearl-framework/pearl'

export async function CorsMiddleware(ctx: HttpContext, next: NextFunction) {
  ctx.response.setHeader('Access-Control-Allow-Origin',  '*')
  ctx.response.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')
  ctx.response.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')

  // Preflight requests end here
  if (ctx.request.method === 'OPTIONS') {
    ctx.response.status(204).end()
    return
  }

  await next()
}

Error handler middleware

Register this first (before other middleware) so it wraps every handler. It catches any thrown error and returns a structured JSON response:

src/middleware/ErrorHandlerMiddleware.ts
import type { HttpContext, NextFunction } from '@pearl-framework/pearl'
import { ValidationException } from '@pearl-framework/pearl'

export async function ErrorHandlerMiddleware(ctx: HttpContext, next: NextFunction) {
  try {
    await next()
  } catch (err) {
    // Validation errors from FormRequest get a 422
    if (err instanceof ValidationException) {
      ctx.response.status(422).json({ errors: err.errors })
      return
    }
    // Everything else is a 500
    console.error(err)
    ctx.response.status(500).json({ error: 'Internal server error' })
  }
}

Built-in: rate limiting

Pearl ships a RateLimit middleware out of the box — fixed window, per-key, with X-RateLimit-* and Retry-After headers on 429 responses. The default store is in-memory; swap in a Redis-backed store for multi-process deployments.

···
import { RateLimit } from '@pearl-framework/pearl'

// Global — 100 requests per minute per IP
router.use(new RateLimit({ windowMs: 60_000, max: 100 }))

// Per-route — tight limit on the login endpoint
router.post('/auth/login', loginHandler, [
  new RateLimit({
    windowMs: 15 * 60_000,
    max:      5,
    message:  'Too many login attempts. Try again in 15 minutes.',
  }),
])

Customize the key (e.g. per-user instead of per-IP) by passing keyGenerator:

···
new RateLimit({
  windowMs: 60_000,
  max:      30,
  keyGenerator: (ctx) => {
    const user = ctx.get<{ id: number }>('auth.user')
    return user ? `u:${user.id}` : `ip:${ctx.request.header('x-forwarded-for') ?? 'unknown'}`
  },
})

For multi-process deployments, implement RateLimitStore against Redis and pass it via the store option — the same middleware then rate-limits consistently across every instance.

Applying middleware

Use router.use() for global middleware (runs on every request) and pass an array as the third route argument for per-route middleware:

···
// Global middleware — runs on every request, in order
router.use(ErrorHandlerMiddleware)  // register first — wraps everything
router.use(CorsMiddleware)
router.use(LoggerMiddleware)

// Per-route — only runs on this route
router.post('/posts', createPost, [Authenticate(auth)])

// Stack multiple per-route middleware
router.post('/admin/posts', createPost, [Authenticate(auth), RequireApiKey])