The problem with full-stack TypeScript today — and how one schema definition replaces six.
In a typical TypeScript stack, the same data shape is defined in six different places — each maintained separately, each drifting independently:
interface User — hand-written, no runtime enforcement.
Zod / Joi / Yup schema — separate from the type, easy to forget updates.
Express/Fastify route handler — parses body manually, casts to the type.
fetch('/api/users') with manual type assertion or codegen output.
Hand-written YAML or separate codegen pipeline — drifts from the real implementation.
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.
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:
InferType<typeof User> — inferred, never hand-written.
User.validate(data) with typed error selectors per field.
endpoint.post().body(User) — handler is fully typed, invalid requests rejected automatically.
createClient(api) — zero codegen, types flow from the contract.
Generated at runtime from schema introspection — always matches the real types.
<Field forProperty={f => f.email} />— type-safe selectors, not string names.
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...
}
});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.
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.
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.
@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.
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.