Route contracts
Define each HTTP operation once so its URL, validated input, handler type, and generated client call stay aligned.
Declare route contracts with the rouzer/http subpath. A route contract is the
shared source of truth for server handler types, client action types, URL
construction, and request validation.
import { $type, metadata } from 'rouzer'
import * as http from 'rouzer/http'
import * as z from 'zod'
type Profile = {
id: string
name: string
}
export const profiles = http.resource('profiles/:id', {
...metadata({ description: 'Profile operations' }),
get: http.get({
query: z.object({
includePosts: z.optional(z.boolean()),
}),
response: $type<Profile>(),
}),
update: http.patch({
body: z.object({
name: z.string().check(z.minLength(1)),
}),
response: $type<Profile>(),
}),
})
export const routes = { profiles }
Resources
Use http.resource(path, children) when actions share a path prefix or when the
client API should be namespaced.
export const organizations = http.resource('orgs/:orgId', {
members: http.resource('members/:memberId', {
get: http.get({ response: $type<Member>() }),
remove: http.delete({}),
}),
})
await client.organizations.members.get({
orgId: 'acme',
memberId: '42',
})
Resource keys are API names. They do not affect the URL. Resource path patterns and action-local path patterns are joined to produce the final route path.
Actions
Use an action for each HTTP operation:
| Helper | Request schemas | Notes |
|---|---|---|
http.get(...) |
path, query, headers, response |
GET actions do not accept request bodies. |
http.post(...) |
path, body, headers, response |
Mutation action. No query schema. |
http.put(...) |
path, body, headers, response |
Mutation action. No query schema. |
http.patch(...) |
path, body, headers, response |
Mutation action. No query schema. |
http.delete(...) |
path, body, headers, response |
Mutation action. No query schema. |
Each helper accepts either (schema) or (path, schema).
export const listProfiles = http.get('profiles', {
query: z.object({ page: z.optional(z.number()) }),
response: $type<Profile[]>(),
})
export const getProfile = http.get({
response: $type<Profile>(),
})
The HTTP action API models explicit operations. It does not have an ALL
fallback route. Declare each supported method directly.
Path Patterns
Rouzer uses @remix-run/route-pattern for path patterns. Common patterns
include:
profiles/:idv:major.:minorapi(/v:major(.:minor))assets/*pathsearch?q
If you omit a path schema, TypeScript infers path params from the route
pattern and server handlers receive strings. Add a Zod path schema when you
need runtime validation, transforms, or non-string handler types.
export const profile = http.get('profiles/:id', {
path: z.object({
id: z.uuid(),
}),
response: $type<Profile>(),
})
Full URL patterns can be used for top-level actions. Keep full URL patterns out
of resource/base-path composition because resources and router basePath
compose path segments.
Request Schemas
Request schemas are Zod objects except for body: http.rawBody().
export const updateProfile = http.patch('profiles/:id', {
path: z.object({ id: z.uuid() }),
body: z.object({ name: z.string() }),
headers: z.object({
'content-type': z.literal('application/json'),
}),
response: $type<Profile>(),
})
On the client, path, query, and JSON body fields are flattened into the first action argument.
Important
Keep field names unique across path, query, and JSON body schemas. A flat client input cannot represent separate values for the same key.
await client.updateProfile({
id: 'a0f12d72-24e5-4eb0-bb70-6345f574e62a',
name: 'Ada',
})
Per-request RequestInit options, including headers and abort signals, are
passed as the second argument.
Raw Bodies
Use http.rawBody() when an action should pass a BodyInit through to fetch
without JSON encoding.
export const uploadAvatar = http.post('profiles/:id/avatar', {
body: http.rawBody(),
headers: z.object({ 'content-type': z.string() }),
})
await client.uploadAvatar(
{ id: '42' },
{ body: file, headers: { 'content-type': file.type } }
)
Note
Raw-body argument placement depends on route input. With path or query input,
pass the body as options.body; without route input, pass the body as the
first argument.
For a raw-body route without path or query input, the generated client accepts the body as the first argument.
export const upload = http.post('uploads', {
body: http.rawBody(),
})
await client.upload(file, {
headers: { 'content-type': file.type },
})
Server handlers for raw-body routes read from ctx.request with Fetch APIs such
as arrayBuffer(), blob(), formData(), or text(). Rouzer does not parse
or validate raw request bodies.
Metadata
Use metadata(...) to attach optional runtime metadata to resources or actions.
Metadata does not affect routing, validation, client typing, or handler
behavior.
export const sessions = http.resource('sessions', {
...metadata({ description: 'Session control' }),
list: http.post('list', {
...metadata({ description: 'List sessions' }),
response: $type<SessionList>(),
}),
})
Constructed route nodes expose metadata through node.metadata. Use it for
generated documentation, CLIs, route inspectors, or application-specific
tooling.