Why Cleverbrush?

The problem with full-stack TypeScript today — and how one schema definition replaces six.

The problem: types everywhere, in sync nowhere

In a typical TypeScript stack, the same data shape is defined in six different places — each maintained separately, each drifting independently:

1️⃣TypeScript types

interface User — hand-written, no runtime enforcement.

2️⃣Validation

Zod / Joi / Yup schema — separate from the type, easy to forget updates.

3️⃣Server endpoint

Express/Fastify route handler — parses body manually, casts to the type.

4️⃣Client fetch call

fetch('/api/users') with manual type assertion or codegen output.

5️⃣OpenAPI spec

Hand-written YAML or separate codegen pipeline — drifts from the real implementation.

6️⃣Form validation

React Hook Form / Formik with string field names and duplicated rules.

Every time a field changes, you update six files. Miss one and you get a runtime bug, a stale API doc, or a form that silently accepts bad data.

The solution: define once, use everywhere

With Cleverbrush, you define your data shape once using @cleverbrush/schema. That single definition drives every layer of your stack:

import { object, string, number } from '@cleverbrush/schema';

// One definition — the single source of truth
const User = object({
  id:    string().uuid(),
  name:  string().minLength(2),
  email: string().email(),
  age:   number().min(0).max(150)
});

From this single schema, you automatically get:

🔷TypeScript types

InferType<typeof User> — inferred, never hand-written.

Runtime validation

User.validate(data) with typed error selectors per field.

Typed server endpoint

endpoint.post().body(User) — handler is fully typed, invalid requests rejected automatically.

🔗Typed client

createClient(api) — zero codegen, types flow from the contract.

📄OpenAPI 3.1 spec

Generated at runtime from schema introspection — always matches the real types.

📝React forms

<Field forProperty={f => f.email} />— type-safe selectors, not string names.

What makes Cleverbrush different

Contract-first, not server-first

You define your API contract with defineApi() in a shared module. Both the server and client import the same contract. mapHandlers() produces a compile errorif any endpoint is missing a handler — you can't ship an incomplete API.

// Server: mapHandlers ensures every endpoint has a handler
mapHandlers(server, api, {
  users: {
    list:   async () => db.users.findAll(),
    create: async ({ body }) => db.users.insert(body),
    // TS Error: Property 'get' is missing in type...
  }
});

Zero-codegen typed client

No build step. No generated files. The client is a runtime Proxythat reads endpoint metadata from the contract. Types flow through TypeScript's type system directly.

Built-in resilience

The client ships with retry (exponential backoff + jitter), timeout, request deduplication, response caching, and request batching — all configurable globally or per call. No competing framework bundles all of this.

Integrated auth & DI

Authentication (JWT, cookies, OAuth2, OIDC) and dependency injection (.NET-style with schemas as service keys) are first-class — not third-party plugins. They plug into the endpoint builder with full type safety.

Enterprise-grade OpenAPI

@cleverbrush/server-openapigenerates OpenAPI 3.1 specs with features most tools lack: typed response links, callbacks, webhooks, response headers, and security schemes. The spec is always accurate because it's generated from the same schemas your code uses.

Schema as the universal key

The same schema instance works as a DI service key, a server endpoint type, an API contract type, a form validator, and an OpenAPI source. No other framework in the TypeScript ecosystem provides this level of cross-concern schema integration.