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