Documentation Index
Fetch the complete documentation index at: https://mintlify.com/elysiajs/documentation/llms.txt
Use this file to discover all available pages before exploring further.
Macros let you package lifecycle events, schema validation, and context extensions into reusable route options. Once you define a macro on a plugin, its properties become available in any route’s hook object — keeping route definitions concise while centralizing shared logic.
Defining a macro
Use .macro() to declare named properties that map to lifecycle behavior. Each property receives the value passed in the route hook and returns a lifecycle configuration object.
import { Elysia } from 'elysia'
const plugin = new Elysia({ name: 'plugin' })
.macro({
hi: (word: string) => ({
beforeHandle() {
console.log(word)
}
})
})
const app = new Elysia()
.use(plugin)
.get('/', () => 'hi', {
hi: 'Elysia'
})
Accessing that route logs "Elysia" to the console.
Property shorthand
Starting from Elysia 1.2.10, a macro property can be an object instead of a function. When it is an object, it behaves as a function that accepts a boolean and runs only when true.
import { Elysia } from 'elysia'
export const auth = new Elysia()
.macro({
// Object shorthand — runs when isAuth: true
isAuth: {
resolve: () => ({
user: 'saltyaom'
})
}
})
Error handling
Return a status from a macro’s lifecycle to send an HTTP error response. Using return status(...) is preferred over throw to preserve correct type inference for Eden and OpenAPI.
import { Elysia, status } from 'elysia'
new Elysia()
.macro({
auth: {
resolve({ headers }) {
if (!headers.authorization)
return status(401, 'Unauthorized')
return {
user: 'SaltyAom'
}
}
}
})
.get('/', ({ user }) => `Hello ${user}`, {
auth: true
})
If you throw new Error() inside a macro instead of returning a status, Elysia converts it to 500 Internal Server Error and type inference for Eden will not reflect the correct status code.
Resolve
Return a resolve function from your macro to inject new properties into the request context. This is useful for authentication, database lookups, or any per-request data enrichment.
import { Elysia } from 'elysia'
new Elysia()
.macro({
user: (enabled: true) => ({
resolve: () => ({
user: 'Pardofelis'
})
})
})
.get('/', ({ user }) => user, {
user: true
})
Common use cases for resolve in macros:
- Authenticate a token and attach the user object to context
- Run a database query and attach results to context
- Decode and validate a session
Extending macros with resolve
TypeScript cannot infer types across macro extensions. Use the named single-macro form as a workaround.
import { Elysia, t } from 'elysia'
new Elysia()
.macro('user', {
resolve: () => ({
user: 'lilith' as const
})
})
.macro('user2', {
user: true,
resolve: ({ user }) => {
// `user` is correctly typed here
}
})
Schema
A macro can attach a schema to every route that activates it. The macro schema is merged with any schema already defined on the route.
import { Elysia, t } from 'elysia'
new Elysia()
.macro({
withFriends: {
body: t.Object({
friends: t.Tuple([t.Literal('Fouco'), t.Literal('Sartre')])
})
}
})
.post('/', ({ body }) => body.friends, {
body: t.Object({
name: t.Literal('Lilith')
}),
withFriends: true
})
Multiple macros can contribute schemas to the same route simultaneously without conflict.
Schema with lifecycle in the same macro
To get type inference for lifecycle events within the same macro, use the named single-macro form.
import { Elysia, t } from 'elysia'
new Elysia()
.macro('withFriends', {
body: t.Object({
friends: t.Tuple([t.Literal('Fouco'), t.Literal('Sartre')])
}),
beforeHandle({ body: { friends } }) {
// `friends` is typed correctly here
}
})
Extension
Macros can reference other macros defined in the same .macro() call, enabling composition.
import { Elysia, t } from 'elysia'
new Elysia()
.macro({
sartre: {
body: t.Object({
sartre: t.Literal('Sartre')
})
},
fouco: {
body: t.Object({
fouco: t.Literal('Fouco')
})
},
lilith: {
fouco: true,
sartre: true,
body: t.Object({
lilith: t.Literal('Lilith')
})
}
})
.post('/', ({ body }) => body, {
lilith: true
})
Deduplication
Elysia deduplicates lifecycle events automatically so the same handler is never registered twice. The deduplication seed defaults to the property value, but you can override it.
import { Elysia, t } from 'elysia'
new Elysia()
.macro({
sartre: (role: string) => ({
seed: role,
body: t.Object({
sartre: t.Literal('Sartre')
})
})
})
Elysia limits macro extension depth to 16 levels to prevent infinite loops in both runtime and type inference.