Skip to content
fxdavePublic

About

Typesharing between frontend and backends made easy. The missing type-safety for full-stack.

Resources

Stars

10 stars

Watchers

1 watching

Forks

Repository files navigation

Cuple

Cuple RPC

End-to-end type safety for TypeScript, without giving up REST.
Define a route on the server, call it from the client. Params, query, headers, body
and every error response are typed.

npm license docs

Docs · Try it on StackBlitz · Boilerplate

Why Cuple

  • Errors are part of the type. Every response the handler can return (success, post-not-found, invalid-body, ...) is a discriminated union on the client. You handle failures the compiler knows about, not catch (e: unknown). Return them with notFound, unauthorized, forbidden and conflict, and name each one when the client needs to tell them apart.
  • No lock-in. Give a route a .path() and it's also a regular REST endpoint, documented by an OpenAPI spec generated from the same definition. Other teams and third parties can use your API without Cuple.
  • The whole request is typed. URL params, query strings and headers are validated and typed, not just the body.
  • Just Express. Cuple routes sit next to your existing ones. Logging, CORS, sessions and middlewares keep working.
  • Batteries included. React 19 bindings with Suspense and a cache, OpenAPI 3.0 generation, and MCP tools for AI agents, all from the same route definitions. More are on the way.

Quick look

npm i @cuple/server @cuple/client express zod

Server

const builder = createBuilder(app);

export const routes = {
  getPost: builder
    .path("/post/:id") // optional, for REST clients
    .paramsSchema(z.strictObject({ id: z.coerce.number() }))
    .get(async ({ data }) => {
      const post = await findPost(data.params.id);
      if (!post) return notFound({ result: "post-not-found", message: "No such post" });
      return success({ post });
    }),
};

initRpc(app, { path: "/rpc", routes });

Client

const client = createClient<typeof routes>({ path: "http://localhost:8080/rpc" });

// Like fetch, every response resolves, typed as every result the route can send.
const res = await fetchCuple(client.getPost.get, { params: { id: 1 } });
if (res.result === "post-not-found") console.log(res.message);

// Keep only success; any other result throws.
const { post } = await fetchCuple(client.getPost.get, { params: { id: 1 } }).thenKeepSuccess();

React

function Post({ id }: { id: number }) {
  const { post } = useGet(client.getPost, { params: { id } }); // suspends until loaded
  return <h1>{post.title}</h1>;
}

Rename a field on the server and the client stops compiling. Add a new error result and TypeScript shows you every place that should handle it.

Packages

Package
@cuple/server Express integration: routes, validation, middlewares, SSE
@cuple/client Type-safe client: fetchCuple, fetchCupleSSE
@cuple/react React 19 bindings: Suspense reads, actions, pagination, a cache
@cuple/openapi Generate OpenAPI 3.0 specs from your routes
@cuple/mcp Expose your routes as MCP tools
@cuple/inspect Route metadata at build time (used by openapi and mcp)

Get started

Used by

RolloutIt

Open source:

  • PhotoBin: temporary photo albums with end-to-end encryption
  • DavidHomeVentory: household inventory with QR-tagged boxes and an Android client

License

MIT

About

Typesharing between frontend and backends made easy. The missing type-safety for full-stack.

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages