7.3 KiB
ElysiaJS: Routing, Handlers & Context
Routing
Path Types
new Elysia()
.get('/static', 'static path') // exact match
.get('/id/:id', 'dynamic path') // captures segment
.get('/id/*', 'wildcard path') // captures rest
Path Priority: static > dynamic > wildcard
Dynamic Paths
new Elysia()
.get('/id/:id', ({ params: { id } }) => id)
.get('/id/:id/:name', ({ params: { id, name } }) => id + ' ' + name)
Optional params: .get('/id/:id?', ...)
HTTP Verbs
.get()- retrieve data.post()- submit/create.put()- replace.patch()- partial update.delete()- remove.all()- any method.route(method, path, handler)- custom verb
Grouping Routes
new Elysia()
.group('/user', { body: t.Literal('auth') }, (app) =>
app.post('/sign-in', ...)
.post('/sign-up', ...)
)
// Or use prefix in constructor
new Elysia({ prefix: '/user' })
.post('/sign-in', ...)
Handlers
Handler = function accepting HTTP request, returning response
// Inline value (compiled ahead, optimized)
.get('/', 'Hello Elysia')
.get('/video', file('video.mp4'))
// Function handler
.get('/', () => 'hello')
.get('/', ({ params, query, body }) => {...})
Context Properties
body- HTTP message/form/filequery- query string as objectparams- path parametersheaders- HTTP headerscookie- mutable signal for cookiesstore- global mutable staterequest- Web Standard Requestserver- Bun server instancepath- request pathname
Context Utilities
import { redirect, form } from 'elysia'
new Elysia().get('/', ({ status, set, form }) => {
// Status code (type-safe)
status(418, "I'm a teapot")
// Set response props
set.headers['x-custom'] = 'value'
set.status = 418 // legacy, no type inference
// Redirect
return redirect('https://...', 302)
// Cookies (mutable signal, no get/set)
cookie.name.value // get
cookie.name.value = 'new' // set
// FormData response
return form({ name: 'Party', images: [file('a.jpg')] })
// Single file
return file('document.pdf')
})
Streaming
new Elysia()
.get('/stream', function* () {
yield 1
yield 2
yield 3
})
// Server-Sent Events
.get('/sse', function* () {
yield sse('hello')
yield sse({ event: 'msg', data: {...} })
})
Note: Headers only settable before first yield
Conditional stream: returning without yield converts to normal response
Context Extension
[Inference] Extend when property is:
- Global mutable (use
state) - Request/response related (use
decorate) - Derived from existing props (use
derive/resolve)
state() - Global Mutable
new Elysia()
`.state('version', 1)
.get('/', ({ store: { version } }) => version)
// Multiple
.state({ counter: 0, visits: 0 })
// Remap (create new from existing)
.state(({ version, ...store }) => ({
...store,
apiVersion: version
}))
Gotcha: Use reference not value
new Elysia()
// ✅ Correct
.get('/', ({ store }) => store.counter++)
// ❌ Wrong - loses reference
.get('/', ({ store: { counter } }) => counter++)
decorate() - Additional Context Props
new Elysia()
.decorate('logger', new Logger())
.get('/', ({ logger }) => logger.log('hi'))
// Multiple
.decorate({ logger: new Logger(), db: connection })
When: constant/readonly values, classes with internal state, singletons
derive() - Create from Existing (Transform Lifecycle)
new Elysia()
.derive(({ headers }) => ({
bearer: headers.authorization?.startsWith('Bearer ')
? headers.authorization.slice(7)
: null
}))
.get('/', ({ bearer }) => bearer)
Timing: runs at transform (before validation)
Type safety: request props typed as unknown
resolve() - Type-Safe Derive (beforeHandle Lifecycle)
new Elysia()
.guard({
headers: t.Object({
bearer: t.String({ pattern: '^Bearer .+$' })
})
})
.resolve(({ headers }) => ({
bearer: headers.bearer.slice(7) // typed correctly
}))
Timing: runs at beforeHandle (after validation) Type safety: request props fully typed
Error from derive/resolve
new Elysia()
.derive(({ headers, status }) => {
if (!headers.authorization) return status(400)
return { bearer: ... }
})
Returns early if error returned
Patterns
Affix (Bulk Remap)
const plugin = new Elysia({ name: 'setup' }).decorate({
argon: 'a',
boron: 'b'
})
new Elysia()
.use(plugin)
.prefix('decorator', 'setup') // setupArgon, setupBoron
.prefix('all', 'setup') // remap everything
Assignment Patterns
- key-value:
.state('key', value) - object:
.state({ k1: v1, k2: v2 }) - remap:
.state(({old}) => ({new}))
Testing
const app = new Elysia().get('/', 'hi')
// Programmatic test
app.handle(new Request('http://localhost/'))
To Throw or Return
Most of an error handling in Elysia can be done by throwing an error and will be handle in onError.
But for status it can be a little bit confusing, since it can be used both as a return value or throw an error.
It could either be return or throw based on your specific needs.
- If an
statusis throw, it will be caught byonErrormiddleware. - If an
statusis return, it will be NOT caught byonErrormiddleware.
See the following code:
import { Elysia, file } from 'elysia'
new Elysia()
.onError(({ code, error, path }) => {
if (code === 418) return 'caught'
})
.get('/throw', ({ status }) => {
// This will be caught by onError
throw status(418)
})
.get('/return', ({ status }) => {
// This will NOT be caught by onError
return status(418)
})
To Throw or Return
Elysia provide a status function for returning HTTP status code, prefers over set.status.
status can be import from Elysia but preferably extract from route handler Context for type safety.
import { Elysia, status } from 'elysia'
function doThing() {
if (Math.random() > 0.33) return status(418, "I'm a teapot")
}
new Elysia().get('/', ({ status }) => {
if (Math.random() > 0.33) return status(418)
return 'ok'
})
Error Handling in Elysia can be done by throwing an error and will be handle in onError.
Status could either be return or throw based on your specific needs.
- If an
statusis throw, it will be caught byonErrormiddleware. - If an
statusis return, it will be NOT caught byonErrormiddleware.
See the following code:
import { Elysia, file } from 'elysia'
new Elysia()
.onError(({ code, error, path }) => {
if (code === 418) return 'caught'
})
.get('/throw', ({ status }) => {
// This will be caught by onError
throw status(418)
})
.get('/return', ({ status }) => {
// This will NOT be caught by onError
return status(418)
})
Notes
[Inference] Based on docs patterns:
- Use inline values for static resources (performance optimization)
- Group routes by prefix for organization
- Extend context minimally (separation of concerns)
- Use
status()overset.statusfor type safety - Prefer
resolve()overderive()when type integrity matters