Skip to content
TypeWire

@tahanabavi/typefetch

v2.0.0

A strongly typed TypeScript HTTP client powered by Zod contracts, middleware, retries, mock data, contract testing, CLI tooling, and report generation.

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.