What are macros?
Macros are meta-programming tools that transform schema definitions at compile time, adding custom validation, transformation, or type information.import { Elysia } from 'elysia'
const app = new Elysia()
.macro(({ onBeforeHandle }) => ({
// Define custom schema properties
role(role: 'admin' | 'user') {
onBeforeHandle(({ error, user }) => {
if (user.role !== role)
return error(403, 'Forbidden')
})
}
}))
.get('/admin', () => 'Admin only', {
role: 'admin' // Custom schema property
})
Defining macros
Macros are defined using the.macro() method:
import { Elysia, t } from 'elysia'
const app = new Elysia()
.macro(({ onBeforeHandle, onAfterHandle }) => ({
// Example: Rate limiting macro
rateLimit(limit: number) {
const requests = new Map<string, number[]>()
onBeforeHandle(({ request, error }) => {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
const now = Date.now()
const window = 60000 // 1 minute
const userRequests = requests.get(ip) ?? []
const recentRequests = userRequests.filter(t => now - t < window)
if (recentRequests.length >= limit) {
return error(429, 'Too many requests')
}
requests.set(ip, [...recentRequests, now])
})
},
// Example: Response transformation
transform(fn: (value: unknown) => unknown) {
onAfterHandle(({ response }) => {
return fn(response)
})
}
}))
Macro lifecycle hooks
Macros have access to all lifecycle hooks:import { Elysia } from 'elysia'
const app = new Elysia()
.macro((hooks) => {
// Available hooks:
const {
onParse,
onTransform,
onBeforeHandle,
onAfterHandle,
onError,
onResponse
} = hooks
return {
log(message: string) {
onBeforeHandle(() => {
console.log(`[Before] ${message}`)
})
onAfterHandle(() => {
console.log(`[After] ${message}`)
})
}
}
})
.get('/test', () => 'Hello', {
log: 'Test endpoint called'
})
Type-safe macros
Macros are fully integrated with TypeScript’s type system:import { Elysia, t } from 'elysia'
const app = new Elysia()
.macro(() => ({
// Macro with typed parameters
cache(options: { ttl: number; key?: string }) {
const cache = new Map<string, { value: unknown; expires: number }>()
return {
onBeforeHandle({ request, response }) {
const key = options.key ?? request.url
const cached = cache.get(key)
if (cached && cached.expires > Date.now()) {
return cached.value
}
},
onAfterHandle({ response }) {
const key = options.key ?? 'default'
cache.set(key, {
value: response,
expires: Date.now() + options.ttl
})
}
}
}
}))
.get('/data', () => ({ data: 'expensive computation' }), {
cache: { ttl: 60000 } // Type-checked
})
Macro composition
Macros can be composed and reused across routes:import { Elysia } from 'elysia'
// Define reusable macros
const authMacro = new Elysia()
.macro(({ onBeforeHandle }) => ({
auth(required = true) {
onBeforeHandle(({ headers, error }) => {
if (required && !headers.authorization) {
return error(401, 'Unauthorized')
}
})
}
}))
const validationMacro = new Elysia()
.macro(({ onBeforeHandle }) => ({
validate(schema: Record<string, unknown>) {
onBeforeHandle(({ body, error }) => {
for (const [key, value] of Object.entries(schema)) {
if (!(key in (body as Record<string, unknown>))) {
return error(400, `Missing field: ${key}`)
}
}
})
}
}))
// Use composed macros
const app = new Elysia()
.use(authMacro)
.use(validationMacro)
.post('/protected', ({ body }) => body, {
auth: true,
validate: { name: 'string', email: 'string' }
})
Context-aware macros
Macros can access and modify the request context:import { Elysia, t } from 'elysia'
const app = new Elysia()
.decorate('metrics', {
requests: 0,
errors: 0
})
.macro(({ onBeforeHandle, onAfterHandle, onError }) => ({
track(name: string) {
onBeforeHandle(({ metrics, store }) => {
metrics.requests++
store.startTime = Date.now()
})
onAfterHandle(({ store }) => {
const duration = Date.now() - (store.startTime as number)
console.log(`${name} took ${duration}ms`)
})
onError(({ metrics }) => {
metrics.errors++
})
}
}))
.get('/api', () => 'Response', {
track: 'API Call'
})
Schema macros
Macros can modify or validate schemas:import { Elysia, t } from 'elysia'
const app = new Elysia()
.macro(() => ({
// Add default schema behavior
strict(enabled = true) {
return {
schema: {
additionalProperties: !enabled
}
}
},
// Transform response format
paginated(pageSize = 10) {
return {
onAfterHandle({ response }) {
if (Array.isArray(response)) {
return {
data: response.slice(0, pageSize),
pageSize,
total: response.length
}
}
return response
}
}
}
}))
.get('/items', () => [1, 2, 3, 4, 5], {
paginated: 3
})
// Returns: { data: [1, 2, 3], pageSize: 3, total: 5 }
Macro metadata
Macros can store metadata for documentation or tooling:import { Elysia } from 'elysia'
const app = new Elysia()
.macro(() => ({
// Documentation macro
tag(...tags: string[]) {
return {
detail: {
tags
}
}
},
// Deprecation macro
deprecated(message?: string) {
return {
detail: {
deprecated: true,
description: message ?? 'This endpoint is deprecated'
}
}
}
}))
.get('/old-api', () => 'Legacy response', {
deprecated: 'Use /v2/api instead',
tag: 'legacy', 'deprecated'
})
Performance macros
Macros for performance monitoring and optimization:import { Elysia } from 'elysia'
const app = new Elysia()
.macro(({ onBeforeHandle, onAfterHandle }) => ({
// Performance monitoring
benchmark(label: string) {
onBeforeHandle(({ store }) => {
store.benchmarkStart = performance.now()
console.time(label)
})
onAfterHandle(({ store }) => {
console.timeEnd(label)
const elapsed = performance.now() - (store.benchmarkStart as number)
if (elapsed > 1000) {
console.warn(`⚠️ Slow endpoint: ${label} (${elapsed}ms)`)
}
})
},
// Response compression hint
compress(minSize = 1024) {
onAfterHandle(({ response, set }) => {
const size = JSON.stringify(response).length
if (size >= minSize) {
set.headers['content-encoding'] = 'gzip'
}
})
}
}))
.get('/heavy', () => ({ data: new Array(1000).fill('data') }), {
benchmark: 'Heavy endpoint',
compress: 500
})
Error handling macros
Create custom error handling logic:import { Elysia } from 'elysia'
const app = new Elysia()
.macro(({ onError }) => ({
retry(maxAttempts = 3) {
return {
onError({ error, request, set }) {
const attempts = parseInt(
request.headers.get('x-retry-count') ?? '0'
)
if (attempts < maxAttempts) {
set.headers['x-retry-count'] = String(attempts + 1)
// Trigger retry logic
} else {
return error
}
}
}
},
fallback(value: unknown) {
return {
onError() {
return value
}
}
}
}))
.get('/unreliable', () => {
if (Math.random() > 0.5) throw new Error('Random failure')
return 'Success'
}, {
retry: 3,
fallback: { error: 'Service temporarily unavailable' }
})
Macro scope
Macros can be scoped to specific routes or groups:import { Elysia } from 'elysia'
const app = new Elysia()
// Global macro
.macro(() => ({
global(value: string) {
console.log('Global:', value)
}
}))
// Scoped macros
.group('/admin', (app) =>
app
.macro(() => ({
adminOnly() {
// Only available in /admin routes
}
}))
.get('/users', () => 'Users', {
adminOnly: true
})
)
Best practices
Keep macros focused
// Good: Single responsibility
.macro(() => ({
rateLimit(limit: number) {
// Only handles rate limiting
}
}))
// Avoid: Multiple responsibilities
.macro(() => ({
everything(options: ComplexOptions) {
// Too many concerns
}
}))
Type your macro parameters
// Good: Typed parameters
.macro(() => ({
timeout(ms: number) {
// TypeScript enforces number type
}
}))
// Avoid: Untyped parameters
.macro(() => ({
timeout(ms: any) {
// No type safety
}
}))
Document macro behavior
/**
* Rate limiting macro
*
* @param limit - Maximum requests per minute
* @example
* ```ts
* .get('/api', handler, { rateLimit: 100 })
* ```
*/
.macro(() => ({
rateLimit(limit: number) {
// Implementation
}
}))
Compose macros for reusability
// Create macro library
const macros = new Elysia()
.macro(() => ({
auth() { /* ... */ },
rateLimit(n: number) { /* ... */ },
cache(ttl: number) { /* ... */ }
}))
// Reuse across apps
const app = new Elysia()
.use(macros)
.get('/protected', handler, {
auth: true,
rateLimit: 100,
cache: 60000
})