// 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.
The contract, in full
// 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
// 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);