Typed Requests
One of Hedystia's core strengths is that the client knows exactly what every route expects — body shape, query params, path params — and TypeScript enforces these at compile time.
How Typing Works
When you call createClient<typeof app>(), TypeScript extracts the route definitions from your server instance type. The client proxy is then typed according to those definitions.
This means:
- Passing the wrong body shape → TypeScript error
- Accessing a non-existent route → TypeScript error
- Supplying the wrong param type → TypeScript error
No runtime code generation. No schema sync step. The types update the moment you change your server.
Body Typing
ts
import { createClient } from '@hedystia/client'
import type { app } from './server'
const client = createClient<typeof app>('http://localhost:3000')
// Client — TypeScript enforces the exact shape
const { data } = await client.products.post({
body: {
name: 'Widget',
price: 9.99,
tags: ['sale', 'new'], // optional, correctly typed
},
})Param Typing
Path parameters are inferred from the route path and schema:
ts
import { createClient } from '@hedystia/client'
import type { app } from './server'
const client = createClient<typeof app>('http://localhost:3000')
// Client
await client.users.id(42).get() // ✓ TypeScript accepts number
await client.users.id('abc').get() // ✗ TypeScript error: expected numberQuery Typing
ts
import { createClient } from '@hedystia/client'
import type { app } from './server'
const client = createClient<typeof app>('http://localhost:3000')
// Client
await client.search.get({
query: { q: 'hello', limit: 10 },
})Header Typing
ts
import { createClient } from '@hedystia/client'
import type { app } from './server'
const client = createClient<typeof app>('http://localhost:3000')
// Client — TypeScript expects authorization header
await client.secure.get({
headers: { authorization: 'Bearer token' },
})Response Typing
The return type of data is inferred from the response schema:
ts
import { createClient } from '@hedystia/client'
import type { app } from './server'
const client = createClient<typeof app>('http://localhost:3000')
// Client — TypeScript knows the shape of data
const { data } = await client.stats.get()
console.log(data?.users) // typed: number | null
console.log(data?.requests) // typed: number | nullError Typing
ts
import { createClient } from '@hedystia/client'
import type { app } from './server'
const client = createClient<typeof app>('http://localhost:3000')
// Client
const { data, error } = await client.items.id(99).get()
if (error) {
console.log(error.message) // typed: string
console.log(error.code) // typed: number
}Nested Paths
Routes with multi-segment paths map to a chain of properties:
ts
// Server route: GET /api/v1/users/:id/posts
app.group('/api', (app) =>
app.group('/v1', (app) =>
app.group('/users', (app) =>
app.get('/:id/posts', ({ params }) => [], {
params: h.object({ id: h.number().coerce() }),
})
)
)
)
// Client
const client = createClient<typeof app>('')
await client.api.v1.users.id(5).posts.get()Subscription Typing
Subscription callbacks are typed based on the data and error schemas:
ts
import { createClient } from '@hedystia/client'
import type { app } from './server'
const client = createClient<typeof app>('http://localhost:3000')
// Client
client.feed.subscribe(({ data, error }) => {
if (data) {
console.log(data.item) // typed: string
console.log(data.timestamp) // typed: number
}
if (error) {
console.log(error.reason) // typed: string
}
})