Skip to content
TypeWire

@tahanabavi/typefetch

documents v1.10.0

The typed HTTP client every other package is built around.

Defining API Contracts

A TypeFetch contract is a grouped object of modules and endpoints.

ts
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:

ts
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:

ts
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.

KeyPurpose
pathReplaces path parameters like /users/:id
queryBuilds the query string
bodySent as the JSON or form-data body
headersPer-request headers

Example:

ts
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:

ts
await api.user.updateUser({
path: { id: "123" },
query: { notify: true },
headers: {
"X-Tenant": "main",
},
body: {
name: "Taha",
},
});

TypeFetch sends:

ts
PATCH /users/123?notify=true

With body:

json
{
"name": "Taha"
}

Request Schema Helper

You can use makeRequestSchema to make structured request schemas easier to write.

ts
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:

ts
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.

ts
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:

ts
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.