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 const;After 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 const;Usage:
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 const;Backward 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 const;Usage:
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.