@cleverbrush/client
Typed HTTP client for @cleverbrush/server API contracts — Proxy-based, zero codegen, full type inference
💡 Why @cleverbrush/client?
The Problem
Keeping your client-side fetch calls in sync with the server is painful. You either duplicate types by hand, run a code generator, or give up and use any. Every approach has a cost: stale types, extra build steps, or lost safety.
The Solution
@cleverbrush/client reads the same API contract your server already defines and builds a fully typed client at compile time — no codegen, no manual annotations. At runtime a two-level Proxy translates method calls into fetch requests using endpoint metadata.
Defining a Contract
Define your API contract once in a shared package using defineApi() from @cleverbrush/server/contract:
import { endpoint, route, defineApi } from '@cleverbrush/server/contract';
import { object, string, number, array, boolean } from '@cleverbrush/schema';
const Todo = object({
id: number(),
title: string(),
completed: boolean()
});
export const api = defineApi({
todos: {
list: endpoint.get('/api/todos').returns(array(Todo)),
get: endpoint
.get('/api/todos', route({ id: number().coerce() })`/${t => t.id}`)
.returns(Todo),
create: endpoint
.post('/api/todos')
.body(object({ title: string() }))
.returns(Todo),
delete: endpoint
.delete('/api/todos', route({ id: number().coerce() })`/${t => t.id}`)
}
});Creating a Client
import { api } from 'todo-shared';
import { createClient } from '@cleverbrush/client';
const client = createClient(api, {
baseUrl: 'https://api.example.com',
getToken: () => localStorage.getItem('token'),
onUnauthorized: () => { window.location.href = '/login'; },
});Usage
const todos = await client.todos.list();
const todo = await client.todos.get({ params: { id: 1 } });
const created = await client.todos.create({ body: { title: 'Buy milk' } });
await client.todos.delete({ params: { id: 1 } });