Sorbus
Sorbus is a typed fetch client. Apiwork JS generates a Sorbus contract that exposes every endpoint as a typed call — filtering, sorting, pagination, nested writes, and error handling.
Install:
pnpm add sorbus
# or
npm install sorbusFull documentation: sorbus.dev.
Setup
Generate the contract:
Apiwork::API.define '/api/v1' do
export :sorbus do
key_format :camel
end
endCreate the client:
import { createClient } from 'sorbus';
import { contract } from './contract';
const api = createClient(contract, '/api/v1', {
headers: () => ({
Authorization: `Bearer ${getToken()}`,
}),
serializeKey: 'snake',
normalizeKey: 'camel',
});That's it. Every endpoint in the contract is now a typed function on api.
Queries
const { invoices, meta } = await api.invoices.index({
filter: {
status: {
eq: 'paid',
},
issuedOn: {
gte: '2024-01-01',
},
},
sort: {
issuedOn: 'desc',
},
page: {
number: 1,
size: 20,
},
});Every filter operator, enum value, and sort field is typed. Invalid filters fail at compile time — not in production.
Logical Operators
Combine filters with AND, OR, and NOT:
const { invoices } = await api.invoices.index({
filter: {
OR: [
{
status: {
eq: 'draft',
},
},
{
status: {
eq: 'overdue',
},
},
],
},
});Includes
Load associations with typed include:
const { invoice } = await api.invoices.show({
id: '123',
include: {
customer: true,
items: true,
},
});
// invoice.customer and invoice.items are typedMutations
Create
const { invoice } = await api.invoices.create({
invoice: {
number: 'INV-001',
issuedOn: '2024-06-01',
items: [
{
OP: 'create',
description: 'Consulting',
quantity: 10,
rate: 150,
},
],
},
});Update with Nested Writes
Create, update, and delete related records in a single request:
const { invoice } = await api.invoices.update({
id: '123',
invoice: {
items: [
{
OP: 'update',
id: '5',
description: 'Updated item',
},
{
OP: 'create',
description: 'New item',
quantity: 1,
rate: 100,
},
{
OP: 'delete',
id: '3',
},
],
},
});OP determines the operation: 'create', 'update', or 'delete'.
Error Handling
Sorbus throws on non-OK responses by default. Catch specific status codes to get typed error data instead:
const result = await api.invoices.create(
{
invoice: {
number: 'INV-001',
},
},
{
catch: [422],
},
);
if (!result.ok) {
result.data.errors;
// { number?: string[], issuedOn?: string[] }
return;
}
result.data.invoice; // fully typedThe error shape is generated from the contract. Rails validation errors map directly to typed fields.
The Workflow
1. Change your Rails code (add column, change type, add enum value)
2. Regenerate the contract
3. TypeScript tells you what brokeThe database is the source of truth. Apiwork reads it. Sorbus types it. Nothing drifts.
See also
- Sorbus documentation — full client reference
- Sorbus export — export configuration and output format