Typed contracts.
Any transport.
Define your API once as a Zod contract — then validate it end-to-end across HTTP, WebSocket, and the server. One source of truth, wired to everything.
Reads your project, asks what it needs, wires it up.
- 12 packages
- 12 on npm
- 8 with zero runtime deps
- Zod 4
- MIT
- npmNodeBunDeno
// contracts.ts
export 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() }),
},
},
} as const;await client.modules.user.getUser({ path: { id } })
@TypeFetchEndpoint(contracts.user.getUser)
useQuery(contracts.user.getUser, { path: { id } })
one timeline, every wire
one object · four consumers
// WHAT IS TYPEWIRE
One contract. Client, server, cache and devtools all read it.
You describe an endpoint once — method, path, request, response. Every package in the family consumes that same object.
Define it once
A plain object with Zod schemas, imported by the frontend and the backend.
export 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(),
}),
},
},
} as const;Consume it on the client
Input and output are validated with the same schema that types them.
const user = await client.modules.user
.getUser({ path: { id: "123" } });
// user: { id: string; name: string }Implement it on the server
The route is wired from the contract, so it cannot drift from the client.
type Input = InferRequest<
typeof contracts.user.getUser
>;
@TypeFetchEndpoint(contracts.user.getUser)
async getUser(@ContractInput() input: Input) {
return { id: input.path.id, name: "Taha" };
}The route can't drift from the client. The client can't drift from the types. The types can't drift from what's validated at runtime.
The server renames a field
// server, last Tuesdayres.json({ id, fullName })// client, still saystype User = { id: string; name: string }const { name } = await getUser(id)→ undefined, at 2am, in production→ typecheck passed. tests passed.
// contracts.ts — the only editresponse: z.object({id: z.string(),fullName: z.string(),})// client, next typecheck→ Property 'name' does not exist→ caught before it ran
// WHY TYPEWIRE
Built so the pieces cannot drift apart
// PACKAGES
12 packages, one contract
12 published on npm, 0 built and tested here awaiting a first publish. Everything below is read from each package's own manifest and README.
12 of 12 packages
Published is the version on npm right now — this page asks the registry, it does not repeat a number written by hand. Unpublished means built and tested in the repo, awaiting its first publish: not an unfinished package, one that has not been given a version number yet.
Compare all packages →// TRANSPORTS
The contract stays. Only the wire changes.
Same object, four call sites. The tab below switches the transport, not the source of truth.
// contracts.ts — fixed
export 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() }),
},
},
} as const;const client = createClient({ contracts, transport: "http" });
const user = await client.modules.user.getUser({
path: { id: "123" },
});
// GET /users/123 — validated against responseThe http adapter ships inside typefetch — nothing extra to install.
// LIVE FROM THE REPO
The guarantees, as they actually stand
Every number here is fetched from GitHub and npm at build time, revalidated hourly. Where an API declines to answer, the card says so rather than showing a zero.
// SUPPORT
Where to take a problem
Three routes, each with a different front door.
Frequently asked
// BUILT FOR AGENTS
A contract is the best possible input for an AI agent
Machine-readable by construction, validated at runtime, and documented for agents in the repo itself.
// FEEDBACK
The project is young. Tell us what breaks.
No testimonial wall yet — these slots hold real quotes when there are real quotes.
// COLLABORATE
A change that breaks another package cannot merge quietly
Contributing is a five-step flow with two gates behind it.
- fork
- branch
- change + test
- changeset
- PR
Every PR that changes a package's source must include a changeset.
// ROADMAP
Shipped, and what is next
Read from the checklist in the repository README, so it cannot fall behind it.
Monorepo + shared tooling (pnpm · Changesets · CI gates)
typefetch runtime instrumentation & overrides (the devtools seam)
typefetch-query-core + typefetch-react
the React-Query-like layer
type-devtools-core + type-devtools
the cross-transport inspector
type-permission
framework-less capability permissions + optional contract link (NestJS guard)
Pluggable transports
the open TransportRegistry, typefetch-grpc, typefetch-graphql, and a core at zero dependencies
typewire-cli
typewire.config.ts, the project-detecting init wizard, multi-API projects
typewire-nestjs beyond HTTP
gRPC (Connect JSON), GraphQL, and typesocket WS gateways, all from the same contract file
- YOU ARE HERE
12 of 12 packages on npm
typewire snapshot + diffnext up
breaking-change detection against a committed API-surface lockfile
typewire lint · doctor · explain · mock · generate openapi
Connect conformance runner in CI
see docs/ROADMAP.md for why it is still open
type-permission client pre-flight middleware + Vue/React binding recipes
type-opengraph
typed OpenGraph/metadata client
typewire-vue / typewire-angular query adapters
$ npx typewire diff v1.4.0 v2.0.0+ user.getUser.response.email- user.getUser.response.name~ user.listUsers.request.page → cursor3 changes · 1 breaking→ 2 call sites will fail typecheck
Not a shipped command. The CLI ships init, list, test and release-doc today; diff and lint are designed in docs/CLI.md and are what the roadmap above is heading towards.
Define it once.Wire it everywhere.
npx typewire init