https://github.com/skiftle/sorbus
The typed fetch client
https://github.com/skiftle/sorbus
contract fetch rest-api type-safe typescript zod
Last synced: 3 months ago
JSON representation
The typed fetch client
- Host: GitHub
- URL: https://github.com/skiftle/sorbus
- Owner: skiftle
- License: mit
- Created: 2026-02-26T15:58:27.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-19T17:13:19.000Z (3 months ago)
- Last Synced: 2026-03-20T09:04:12.580Z (3 months ago)
- Topics: contract, fetch, rest-api, type-safe, typescript, zod
- Language: TypeScript
- Homepage: https://sorbus.dev
- Size: 211 KB
- Stars: 3
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Sorbus


[](https://www.npmjs.com/package/sorbus)
[](https://github.com/skiftle/sorbus/actions/workflows/ci.yml)
[](LICENSE)
Sorbus is a typed fetch client for APIs where you can't share types directly — Rails, Django, Go, Laravel. Instead of verbose OpenAPI specs and opaque code generators, you define your API as a contract: endpoints with Zod schemas. Params, responses, and errors are all inferred.
The contract is just TypeScript — compose schemas, pick fields for forms, reuse them for validation. Write contracts by hand, or generate them with [Apiwork](https://github.com/skiftle/apiwork) from your Rails API.
A thin layer over `fetch`. No codegen. No opaque output.
See https://sorbus.dev for full documentation.
## Install
```bash
npm install sorbus zod
# or
pnpm add sorbus zod
```
## The Contract
```typescript
import { z } from 'zod';
const InvoiceSchema = z.object({
id: z.string(),
number: z.string(),
total: z.number(),
});
export const contract = {
endpoints: {
invoices: {
show: {
method: 'GET',
path: '/invoices/:id',
pathParams: z.object({
id: z.string(),
}),
response: {
body: z.object({
invoice: InvoiceSchema,
}),
},
},
create: {
method: 'POST',
path: '/invoices',
request: {
body: z.object({
invoice: InvoiceSchema.pick({
number: true,
total: true,
}),
}),
},
response: {
body: z.object({
invoice: InvoiceSchema,
}),
},
errors: [422],
},
},
},
error: z.object({
message: z.string(),
errors: z.record(z.string(), z.array(z.string())).optional(),
}),
} as const;
```
## The Client
```typescript
import { createClient } from 'sorbus';
import { contract } from './contract';
const api = createClient(contract, '/api');
// Errors throw — just use the data
const { invoice } = await api.invoices.show({ id: '123' });
// Catch specific status codes when you need to
const result = await api.invoices.create(
{
invoice: {
number: 'INV-001',
total: 1000,
},
},
{ catch: [422] },
);
if (!result.ok) {
setErrors(result.data.errors);
return;
}
result.data.invoice; // fully typed
```
## Status
Under active development.