Route Handlers
A route handler is a function that receives a context object and returns a response. In Hedystia, handlers are always fully typed based on the schema you provide.
Handler Signature
typescript
(ctx: Context) => Response | any | Promise<Response | any>The Context type is inferred from the route schema.
Basic Handlers
Returning plain objects
The most common pattern — Hedystia serializes it as JSON:
ts
app.get('/users', () => {
return [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
})Returning strings
Returned as text/plain:
ts
import Hedystia from 'hedystia'
const app = new Hedystia()
app.get('/ping', () => 'pong')Returning a full Response
For complete control:
ts
import Hedystia from 'hedystia'
import { Bun } from 'bun'
const app = new Hedystia()
app.get('/download', () => {
const file = Bun.file('./data.csv')
return new Response(file, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename="data.csv"',
},
})
})Accessing Context
ts
app.post(
'/orders',
async ({ body, params, query, headers, set, error }) => {
// Body is typed from body schema
const order = await createOrder(body)
// Set a custom response header
set.headers.set('x-order-id', String(order.id))
return order
},
{
body: h.object({
product: h.string(),
quantity: h.number().min(1),
}),
}
)Async Handlers
Handlers can be async — errors thrown inside will be caught by onError:
ts
import Hedystia from 'hedystia'
const fetchFromDatabase = async () => ({})
const app = new Hedystia()
app.get('/data', async ({ error }) => {
try {
const result = await fetchFromDatabase()
return result
} catch (e) {
error(500, 'Database error')
}
})Handler Patterns
Conditional Responses
ts
app.get(
'/users/:id',
async ({ params, error }) => {
const user = await db.findUser(params.id)
if (!user) error(404, 'User not found')
return user
},
{
params: h.object({ id: h.number().coerce() }),
response: h.object({ id: h.number(), name: h.string() }),
error: h.object({ message: h.string(), code: h.number() }),
}
)Setting Status Codes
ts
import Hedystia, { h } from 'hedystia'
const createUser = (body: any) => ({ id: 1, ...body })
const app = new Hedystia()
app.post(
'/users',
({ body, set }) => {
const user = createUser(body)
set.status(201) // Created
return user
},
{
body: h.object({ name: h.string() }),
}
)Setting Cookies
ts
import Hedystia from 'hedystia'
const authenticate = (u: any, p: any) => 'token'
const app = new Hedystia()
app.post('/login', ({ body, set }) => {
const token = authenticate((body as any).username, (body as any).password)
set.cookies.set('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 1 week
})
return { ok: true }
})Redirects
ts
import Hedystia from 'hedystia'
const app = new Hedystia()
app.get('/old-path', ({ set }) => {
set.status(301)
set.headers.set('Location', '/new-path')
return null
})Handler with Macros
When a macro is enabled, its resolved value is available on the context:
ts
const app = new Hedystia()
.macro({
auth: () => ({
resolve: async (ctx) => {
const token = ctx.req.headers.get('authorization')?.slice(7)
if (!token) ctx.error(401, 'Missing token')
return { userId: 1, roles: ['admin'] }
},
}),
})
.get(
'/dashboard',
async (ctx) => {
const { userId, roles } = await ctx.auth
return { userId, roles, data: 'private data' }
},
{
auth: true,
response: h.object({
userId: h.number(),
roles: h.string().array(),
data: h.string(),
}),
}
)WebSocket Handlers
Define WebSocket behavior alongside HTTP routes:
typescript
app.ws('/chat', {
open(ws) {
console.log('Client connected from', ws.remoteAddress)
ws.send(JSON.stringify({ type: 'welcome' }))
},
message(ws, msg) {
const parsed = JSON.parse(msg.toString())
ws.publish('chat', JSON.stringify({ from: 'server', data: parsed }))
},
close(ws, code, reason) {
console.log(`Disconnected: ${code} ${reason}`)
},
error(ws, error) {
console.error('WebSocket error:', error)
},
})