@cleverbrush/client

Typed HTTP client for @cleverbrush/server API contracts — Proxy-based, zero codegen, full type inference

$npm install @cleverbrush/client @cleverbrush/server @cleverbrush/schema

The server and schema packages are needed for the shared API contract that drives 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 } });