Step 13 of 30
request lifecycle hooks, derive, prefix route modules, plugin composition with .use()
auth, logging, request context — ทำครั้งเดียวที่ lifecycle hook ไม่ใช่ copy-paste ทุก handler
Elysia routes run inside a lifecycle — hooks like onRequest, preHandler, and onError run at fixed points around your handlers. Plugins (.use()) bundle routes, hooks, and state into reusable modules.
Cross-cutting concerns — auth, logging, request context, error mapping — must not be copy-pasted into every handler. Lifecycle hooks give them one interception point; plugins give your API a modular structure that composes without global mutation. This is the same dependency-injection idea you will meet in any serious framework, expressed with functions.
Loading diagram...
Each hook can short-circuit by returning a value.
import { Elysia } from 'elysia'
new Elysia()
.get('/tasks', ({ tasks }) => tasks.list(), {
beforeHandle({ headers, set }) {
if (!headers.authorization) {
set.status = 401
return 'Unauthorized'
}
}
})
.listen(3000)
beforeHandle runs before the handler — a return value stops the chain.
plugin() + .use()// plugins/auth.ts
import { Elysia, t } from 'elysia'
export const authPlugin = new Elysia({ name: 'auth' })
.derive(({ headers }) => {
const token = headers.authorization?.replace('Bearer ', '')
const user = verifyToken(token) // throws on bad token
return { user }
})
// routes/tasks.ts
import { authPlugin } from '../plugins/auth'
export const taskRoutes = new Elysia({ prefix: '/tasks' })
.use(authPlugin)
.get('/', ({ user, tasks }) => tasks.listFor(user.id))
.post('/', ({ body, user, tasks }) => tasks.create(user.id, body), {
body: t.Object({ title: t.String() })
})
derive adds request-scoped values — every route using the plugin gets user for free. { name } makes plugins deduplicate when used twice; prefix scopes the routes.
// index.ts
import { Elysia } from 'elysia'
import { taskRoutes } from './routes/tasks'
import { logPlugin } from './plugins/logger'
const app = new Elysia()
.use(logPlugin)
.use(taskRoutes)
.listen(3000)
new Elysia()
.onError(({ code, set }) => {
if (code === 'VALIDATION') {
set.status = 422
return { error: 'Invalid request body' }
}
if (code === 'NOT_FOUND') {
set.status = 404
return { error: 'Route not found' }
}
set.status = 500
return { error: 'Internal error' }
})
One place to shape every error response your API returns.
{ name }, using the same plugin twice doubles its hooks.derive. Derive cheap, request-scoped context (current user, db handle) — not workflows.index.ts with 40 routes. Split by resource into route files with prefix; mount them in the app.