Middleware
Lacis middleware is file-based. Place a middleware file alongside your route files and Lacis loads it automatically. There are two filename conventions with different scoping behaviors.
The two types
Section titled “The two types”+middleware.global.ts — cascading
Section titled “+middleware.global.ts — cascading”Runs for every route at its directory level and all routes below it. Right for concerns like authentication, logging, or CORS that apply to an entire section of your API.
+middleware.ts — exact path
Section titled “+middleware.ts — exact path”Runs only when the route path matches the directory that contains it. Does not run for child routes.
File structure
Section titled “File structure”Directoryroutes/
- +middleware.global.ts (runs for every route)
- index.ts (GET /)
Directoryapi/
- +middleware.global.ts (runs for /api and all /api/* routes)
- +middleware.ts (runs for /api only — NOT /api/users)
- index.ts (GET /api)
Directoryusers/
- index.ts (GET /api/users)
Directory[id]/
- index.ts (GET /api/users/:id)
For the route GET /api/users, the execution order is:
routes/+middleware.global.tsbeforeRequestroutes/api/+middleware.global.tsbeforeRequest- Route handler
routes/api/+middleware.global.tsafterRequestroutes/+middleware.global.tsafterRequest
routes/api/+middleware.ts does not run for /api/users — only for /api itself.
Each middleware file can export any combination of beforeRequest, afterRequest, and onError.
beforeRequest
Section titled “beforeRequest”Runs before the route handler. Returning false stops the pipeline — the handler and all subsequent middleware are skipped.
// routes/api/+middleware.global.tsimport type { Request, Response } from 'lacis'
export const beforeRequest = async (req: Request, res: Response) => { const token = req.getHeader('authorization') if (!token) { res.status(401).json({ error: 'Unauthorized' }) return false }}You can export an array — handlers run in order, and the first false stops the chain.
export const beforeRequest = [authCheck, rateLimitCheck]afterRequest
Section titled “afterRequest”Runs after the route handler completes.
export const afterRequest = async (req: Request, res: Response) => { console.log(`${req.method} ${req.url}`)}onError
Section titled “onError”Runs when an unhandled error is thrown by any middleware or the route handler.
export const onError = async ( req: Request, res: Response, context: { error: unknown; phase: string }) => { console.error(`[${context.phase}]`, context.error) if (!res.headersSent) { res.status(500).json({ error: 'Internal Server Error' }) }}Registering middleware in createServer
Section titled “Registering middleware in createServer”For middleware that should apply globally, pass it directly to createServer:
import { createServer } from 'lacis'
createServer('./routes', { middleware: { beforeRequest: async (req, res) => { console.log(`--> ${req.method} ${req.url}`) }, afterRequest: async (req, res) => { console.log(`<-- ${req.method} ${req.url}`) }, onError: async (req, res, ctx) => { console.error('Unhandled error:', ctx.error) }, },})Each property accepts a single handler or an array.
Lifecycle hooks
Section titled “Lifecycle hooks”Lifecycle hooks are registered under hooks in createServer.
onNotFound
Section titled “onNotFound”Called when no route matches the request. If the hook sends a response, the default 404 JSON is skipped.
createServer('./routes', { hooks: { onNotFound: async (req, res) => { res.status(404).json({ error: 'Not found', path: req.url }) }, },})onShutdown
Section titled “onShutdown”Called during graceful shutdown on SIGINT, SIGTERM, or SIGHUP. Use it to close database connections or flush buffers.
createServer('./routes', { hooks: { onShutdown: async () => { await db.end() await cache.quit() }, },})onShutdown runs after the server stops accepting connections. Errors inside it are caught and logged — they do not prevent shutdown.