Defining API Contracts
A TypeFetch contract is a grouped object of modules and endpoints.
const contracts = {
user: {
getUser: {
method: 'GET',
path: '/users/:id',
request: z.object({
path: z.object({
id: z.string(),
}),
}),
response: z.object({
id: z.string(),
name: z.string(),
}),
},
createUser: {
method: 'POST',
path: '/users',
request: z.object({
body: z.object({
name: z.string(),
email: z.string().email(),
}),
}),
response: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
},
},
} as constAfter calling client.init(), TypeFetch generates typed methods:
await api.user.getUser({
path: { id: '123' },
})
await api.user.createUser({
body: {
name: 'Taha',
email: 'taha@example.com',
},
})Structured Request Model
The recommended request shape is:
z.object({
path: z.object({}).optional(),
query: z.object({}).optional(),
body: z.any().optional(),
headers: z.record(z.string(), z.string()).optional(),
})Each section has a specific purpose.
| Key | Purpose |
|---|---|
path | Replaces path parameters like /users/:id |
query | Builds the query string |
body | Sent as the JSON or form-data body |
headers | Per-request headers |
Example:
const contracts = {
user: {
updateUser: {
method: 'PATCH',
path: '/users/:id',
request: z.object({
path: z.object({
id: z.string(),
}),
query: z
.object({
notify: z.boolean().optional(),
})
.optional(),
body: z.object({
name: z.string(),
}),
headers: z.record(z.string(), z.string()).optional(),
}),
response: z.object({
id: z.string(),
name: z.string(),
}),
},
},
} as constUsage:
await api.user.updateUser({
path: { id: '123' },
query: { notify: true },
headers: {
'X-Tenant': 'main',
},
body: {
name: 'Taha',
},
})TypeFetch sends:
PATCH /users/123?notify=trueWith body:
{
"name": "Taha"
}Request Schema Helper
You can use makeRequestSchema to make structured request schemas easier to write.
import { z } from 'zod'
import { makeRequestSchema } from '@tahanabavi/typefetch'
const updateUserRequest = makeRequestSchema<
{ id: z.ZodString },
{ notify: z.ZodOptional<z.ZodBoolean> },
z.ZodObject<{
name: z.ZodString
}>
>()({
path: z.object({
id: z.string(),
}),
query: z.object({
notify: z.boolean().optional(),
}),
body: z.object({
name: z.string(),
}),
headers: z.record(z.string(), z.string()).optional(),
})Use it inside an endpoint:
const contracts = {
user: {
updateUser: {
method: 'PATCH',
path: '/users/:id',
request: updateUserRequest,
response: z.object({
id: z.string(),
name: z.string(),
}),
},
},
} as constBackward Compatibility
Flat request schemas are still supported.
const contracts = {
user: {
createUser: {
method: 'POST',
path: '/users',
request: z.object({
name: z.string(),
}),
response: z.object({
id: z.string(),
name: z.string(),
}),
},
},
} as constUsage:
await api.user.createUser({
name: 'Taha',
})For non-GET requests, the full flat input is sent as the JSON body.
For GET requests, flat input is validated but no body is sent.