Files
agent/.agent/skills/tech-stack/elysiajs/references/lifecycle.md

5.3 KiB

Lifecycle

Instead of a sequential process, Elysia's request handling is divided into multiple stages called lifecycle events.

It's designed to separate the process into distinct phases based on their responsibility without interfering with each others.

List of events in order

  1. request - early, global
  2. parse - body parsing
  3. transform / derive - mutate context pre validation
  4. beforeHandle / resolve - auth/guard logic
  5. handler - your business code
  6. afterHandle - tweak response, set headers
  7. mapResponse - turn anything into a proper Response
  8. onError - centralized error handling
  9. onAfterResponse - post response/cleanup tasks

Request (onRequest)

Runs first for every incoming request.

  • Ideal for caching, rate limiting, CORS, adding global headers.
  • If the hook returns a value, the whole lifecycle stops and that value becomes the response.
new Elysia().onRequest(({ ip, set }) => {
    if (blocked(ip)) return (set.status = 429)
})

Parse (onParse)

Body parsing stage.

  • Handles text/plain, application/json, multipart/form-data, application/x www-form-urlencoded by default.
  • Use to add custom parsers or support extra Content Types.
new Elysia().onParse(({ request, contentType }) => {
    if (contentType === 'application/custom') return request.text()
})

Transform (onTransform)

Runs just before validation; can mutate the request context.

  • Perfect for type coercion, trimming strings, or adding temporary fields that validation will use.
new Elysia().onTransform(({ params }) => {
    params.id = Number(params.id)
})

Derive

Runs along with onTransform but before validation; adds per request values to the context.

  • Useful for extracting info from headers, cookies, query, etc., that you want to reuse in handlers.
new Elysia().derive(({ headers }) => ({
    bearer: headers.authorization?.replace(/^Bearer /, '')
}))

Before Handle (onBeforeHandle)

Executed after validation, right before the route handler.

  • Great for auth checks, permission gating, custom pre validation logic.
  • Returning a value skips the handler.
new Elysia().get('/', () => 'hi', {
    beforeHandle({ cookie, status }) {
        if (!cookie.session) return status(401)
    }
})

Resolve

Like derive but runs after validation along "Before Handle" (so you can rely on validated data).

  • Usually placed inside a guard because it isn't available as a local hook.
new Elysia().guard(
    { headers: t.Object({ authorization: t.String() }) },
    (app) =>
        app
            .resolve(({ headers }) => ({
                bearer: headers.authorization.split(' ')[1]
            }))
            .get('/', ({ bearer }) => bearer)
)

After Handle (onAfterHandle)

Runs after the handler finishes.

  • Can modify response headers, wrap the result in a Response, or transform the payload.
  • Returning a value replaces the handler’s result, but the next afterHandle hooks still run.
new Elysia().get('/', () => '<h1>Hello</h1>', {
    afterHandle({ response, set }) {
        if (isHtml(response)) {
            set.headers['content-type'] = 'text/html; charset=utf-8'
            return new Response(response)
        }
    }
})

Map Response (mapResponse)

Runs right after all afterHandle hooks; maps any value to a Web standard Response.

  • Ideal for compression, custom content type mapping, streaming.
new Elysia().mapResponse(({ responseValue, set }) => {
    const body =
        typeof responseValue === 'object'
            ? JSON.stringify(responseValue)
            : String(responseValue ?? '')

    set.headers['content-encoding'] = 'gzip'
    return new Response(Bun.gzipSync(new TextEncoder().encode(body)), {
        headers: {
            'Content-Type':
                typeof responseValue === 'object'
                    ? 'application/json'
                    : 'text/plain'
        }
    })
})

On Error (onError)

Caught whenever an error bubbles up from any lifecycle stage.

  • Use to customize error messages, handle 404, log, or retry.
  • Must be registered before the routes it should protect.
new Elysia().onError(({ code, status }) => {
    if (code === 'NOT_FOUND') return status(404, '❓ Not found')
    return new Response('Oops', { status: 500 })
})

After Response (onAfterResponse)

Runs after the response has been sent to the client.

  • Perfect for logging, metrics, cleanup.
new Elysia().onAfterResponse(() =>
    console.log('✅ response sent at', Date.now())
)

Hook Types

Type Scope How to add
Local Hook Single route Inside route options (afterHandle, beforeHandle, …)
Interceptor Hook Whole instance (and later routes) .onXxx(cb) or .use(plugin)

Remember: Hooks only affect routes defined after they are registered, except onRequest which is global because it runs before route matching.