Skip to content
TypeWire

// PLAYGROUND

A real project, running on this page

This is not a diagram of TypeWire. It is @tahanabavi/typefetch in your browser, talking to route handlers on this site, both built from one contract object — and a switch that breaks the server so you can watch validation catch it.

the contract

src/features/playground/contracts.ts

Imported by both sides. Nothing else is shared.

the client

src/features/playground/console.tsx

Builds an ApiClient from that object and calls it.

the server

src/app/api/playground/[...path]/route.ts

Implements the same endpoints and validates its own answers.

Call it

Every button below runs a real request through a real client. Hover one to see the exact expression it evaluates.

Make it fail

A one-character name and a malformed email. This never reaches the network — request validation rejects it here in the browser.

Timeline

0 REQUESTS

Fed by client.instrument() — the same hook the devtools package subscribes to.

nothing yet — press a call

The contract, in full

src/features/playground/contracts.ts
// src/features/playground/contracts.ts
export const User = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
role: z.enum(["admin", "member"]),
});

export const contracts = {
user: {
getUser: {
method: "GET",
path: "/users/:id",
request: z.object({ path: z.object({ id: z.string().min(1) }) }),
response: User,
errors: { 404: z.object({ code: z.literal("not_found"), id: z.string() }) },
},

listUsers: {
method: "GET",
path: "/users",
request: z.object({ query: z.object({
role: z.enum(["admin", "member"]).optional(),
limit: z.coerce.number().int().min(1).max(50).default(10),
}) }),
response: z.object({ items: z.array(User), total: z.number() }),
},

createUser: {
method: "POST",
path: "/users",
request: z.object({ body: z.object({
name: z.string().min(2, "name must be at least 2 characters"),
email: z.email("must be a valid email address"),
role: z.enum(["admin", "member"]).default("member"),
}) }),
response: User,
errors: { 409: z.object({ code: z.literal("email_taken"), email: z.string() }) },
},
},
} as const;

How the server uses it

route.ts
// src/app/api/playground/[...path]/route.ts
import { contracts } from "@/features/playground/contracts";

// The route validates its own output against the contract's response schema
// before it answers — the server holds itself to the same object the client
// holds it to.
const parsed = contracts.user.getUser.response.safeParse(record);
if (!parsed.success) return badImplementation(parsed.error);
return Response.json(parsed.data);

What this page is honest about

  • The fixture is a module-level array, shared by everyone and reset whenever the serverless instance recycles. A user you create may be visible to the next visitor, and may vanish a minute later.
  • Only the HTTP transport runs here. GraphQL, gRPC and WebSocket use the same contract object, but their packages are not published yet.
  • You cannot edit the contract from the page. Doing that honestly needs a type-checking compiler in the browser, and a version that only pretends to typecheck would undercut the one claim this project makes.