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' })
  }
}

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])