Build This Now
Build This Now
Qu'est-ce que le code Claude ?Installer Claude CodeL'installateur natif de Claude CodeTon premier projet Claude Code
Claude Code v2.1.122 Release NotesClaude Code Dynamic Workflows : comment orchestrer 1 000 sous-agents sur une vraie codebaseBonnes pratiques Claude CodeMeilleures pratiques pour Claude Opus 4.7Claude Code sur un VPSIntégration GitRevue de code avec Claude CodeLes Worktrees avec Claude CodeClaude Code à distanceClaude Code ChannelsChannels, Routines, Teleport, DispatchTâches planifiées avec Claude CodePermissions Claude CodeLe mode auto de Claude CodeAjouter les paiements Stripe avec Claude CodeFeedback LoopsWorkflows TodoGestion des tâches dans Claude CodeTemplates de projetTarification et utilisation des tokens Claude CodeTarifs de Claude Code : ce que tu vas vraiment payerClaude Code Ultra ReviewConstruire une app Next.js avec Claude CodeClaude Code With Supabase: Database, Auth, RLSVercel deepsec with Claude CodeTest-Driven Development with Claude CodeHow to Build a SaaS MVP With Claude CodeAdding Authentication With Claude Code (Supabase Auth)Adding Transactional Email With Claude Code (Resend + React Email)Building a Type-Safe API With Claude Code (oRPC + Zod)Commerce agentique : comment construire une app que les agents IA peuvent payerClaude Code 1M Context in Practice: When Bigger Isn't BetterClaude Code GitHub Actions Setup Guide (@claude + Cron)Claude Code Headless Mode: The Definitive Guide to claude -pClaude Code Max Plan vs API Cost: Break-Even GuideClaude Code Prompt Caching: The Token Discount Most People Never Turn OnCombien coûte la création d'un SaaS avec Claude Code en 2026Run a Team of AI Agents in Parallel with Git WorktreesPrompt Injection in Coding Agents: How to Not Get Pwned
speedy_devvkoen_salo
Blog/Handbook/Workflow/Building a Type-Safe API With Claude Code (oRPC + Zod)

Building a Type-Safe API With Claude Code (oRPC + Zod)

How to build a fully type-safe API layer in Next.js 16 with oRPC and Zod, using Claude Code, so schema changes break the build instead of production.

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvWritten by speedy_devvPublished Jul 14, 202610 min readHandbook hubWorkflow index

The version of a type-safe API that actually matters isn't a diagram, it's this: you rename a field in your Zod schema, and the client component that reads it turns red in your editor before you've even saved the file. No Postman collection to update. No runtime 500 in production three days later. The error shows up at the exact moment you broke something, in the exact file you need to fix.

That's what oRPC plus Zod gets you in a Next.js 16 app, and Claude Code is a fast way to wire it up correctly on the first pass.


Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →

The bug this setup prevents

The common failure mode in a typical Next.js API route looks like this: you define a route handler that returns { id, title, done }. Somewhere else, a component destructures task.completed because someone renamed the field on the server six weeks ago and nobody updated the client. TypeScript can't catch it, because fetch() returns any (or a type you hand-wrote and now trust blindly). The bug ships. You find out from a Sentry alert, not a compiler.

"Type-safe from database to frontend" means there's exactly one source of truth for the shape of your data, a Zod schema, and both your server handler and your client call are typed off that same schema. Change the schema, and every place that shape flows through lights up with a type error until you fix it.

Installing oRPC and Zod

oRPC ships as a few small packages: a server package for defining procedures and mounting them as a handler, and a client package for calling them with full inference. Install both alongside Zod.

npm install @orpc/server @orpc/client zod

Nothing else is required. No code generation step, no CLI to run after every schema change. The types come from TypeScript inferring through the Zod schema, not from a build artifact you have to remember to regenerate.

Defining the schema

Start with the shape of the thing your API returns. This is a small task list API, one Zod object schema for a task, and a derived schema for what a "create" request accepts (just the title, since id and done get set server-side).

// lib/schemas/task.ts
import * as z from "zod";

export const taskSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(200),
  done: z.boolean(),
});

export const createTaskInput = taskSchema.pick({ title: true });

export type Task = z.infer<typeof taskSchema>;

z.infer is doing the real work here. Task isn't a type you maintain by hand, it's derived from the schema, so it can never drift out of sync with the runtime validator. If you add a field to taskSchema, Task picks it up automatically.

For the tutorial, the "database" is an in-memory array. Swap this for real Supabase or Postgres queries in a production app, the rest of the setup doesn't change.

// lib/db/tasks.ts
import { randomUUID } from "crypto";
import type { Task } from "@/lib/schemas/task";

const tasks: Task[] = [
  { id: randomUUID(), title: "Wire up the API layer", done: false },
];

export async function listTasks(): Promise<Task[]> {
  return tasks;
}

export async function createTask(title: string): Promise<Task> {
  const task: Task = { id: randomUUID(), title, done: false };
  tasks.push(task);
  return task;
}

Defining an oRPC procedure

A procedure in oRPC is built with os, a chainable builder from @orpc/server. You attach an input schema, an output schema, and a handler function. Both schema calls are optional, but attaching both is what gets you validation on the way in and a typed, checked shape on the way out.

// lib/orpc/router.ts
import * as z from "zod";
import { os } from "@orpc/server";
import { taskSchema, createTaskInput } from "@/lib/schemas/task";
import { listTasks, createTask } from "@/lib/db/tasks";

const list = os.output(z.array(taskSchema)).handler(async () => {
  return listTasks();
});

const create = os
  .input(createTaskInput)
  .output(taskSchema)
  .handler(async ({ input }) => {
    return createTask(input.title);
  });

export const router = {
  tasks: {
    list,
    create,
  },
};

export type AppRouter = typeof router;

Two things worth noticing. First, input inside the create handler is already typed as { title: string }, no cast, no manual interface. Second, if createTask returned something that doesn't match taskSchema, TypeScript flags it right there in the handler, before the code ever runs. The output schema is a contract on your own server code, not just on the client.

Mounting the router as a Route Handler

oRPC exposes an RPC handler that speaks plain fetch Request/Response, which maps directly onto a Next.js 16 Route Handler. Mount it on a catch-all segment so every procedure in the router is reachable under one prefix.

// app/rpc/[[...rest]]/route.ts
import { RPCHandler } from "@orpc/server/fetch";
import { router } from "@/lib/orpc/router";

const handler = new RPCHandler(router);

async function handleRequest(request: Request) {
  const { response } = await handler.handle(request, {
    prefix: "/rpc",
    context: {},
  });

  return response ?? new Response("Not found", { status: 404 });
}

export const GET = handleRequest;
export const POST = handleRequest;
export const PUT = handleRequest;
export const PATCH = handleRequest;
export const DELETE = handleRequest;

Every HTTP verb points at the same handleRequest function because the RPC handler figures out which procedure to call from the request itself. You don't write a separate file per endpoint, and you don't hand-map URLs to functions. Adding a new procedure to router in lib/orpc/router.ts is the entire diff.

Calling it from a Server Component

Inside a Server Component, there's no reason to round-trip through HTTP to call your own server. oRPC's server-side client (createRouterClient) calls the procedure's handler function directly, in-process, while keeping the exact same typed interface as the network client.

// lib/orpc/server-client.ts
import "server-only";
import { createRouterClient } from "@orpc/server";
import { router } from "@/lib/orpc/router";

export const orpc = createRouterClient(router, {
  context: {},
});

The server-only import is a guardrail, it throws a build error if this file is ever pulled into client code by accident. Use it in a page:

// app/tasks/page.tsx
import { orpc } from "@/lib/orpc/server-client";

export default async function TasksPage() {
  const tasks = await orpc.tasks.list();

  return (
    <main className="max-w-lg mx-auto py-12">
      <h1 className="text-2xl font-bold mb-6">Tasks</h1>
      <ul className="space-y-2">
        {tasks.map((task) => (
          <li key={task.id}>{task.title}</li>
        ))}
      </ul>
    </main>
  );
}

tasks is Task[], inferred all the way from taskSchema. No manual return type on TasksPage, no any slipping in from an untyped fetch call.

Calling it from a Client Component

The browser can't call your server functions in-process, so the client-side client goes over the wire, using the RPC link pointed at the route you mounted earlier. The interface is identical to the server-side client, same method names, same argument shapes, same inferred return types.

// lib/orpc/client.ts
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import type { RouterClient } from "@orpc/server";
import type { AppRouter } from "@/lib/orpc/router";

const link = new RPCLink({
  url: "/rpc",
});

export const orpc: RouterClient<AppRouter> = createORPCClient(link);

Use it in a form. title is a plain string from useState, and the return value of orpc.tasks.create(...) is a fully typed Task, not a Response you have to .json() and cast.

// components/new-task-form.tsx
"use client";

import { useState } from "react";
import { orpc } from "@/lib/orpc/client";

export function NewTaskForm() {
  const [title, setTitle] = useState("");
  const [pending, setPending] = useState(false);

  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault();
    setPending(true);
    const task = await orpc.tasks.create({ title });
    setPending(false);
    setTitle("");
    console.log("created task", task.id, task.done);
  }

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        value={title}
        onChange={(event) => setTitle(event.target.value)}
        className="border rounded px-3 py-2 flex-1"
        placeholder="New task"
      />
      <button
        type="submit"
        disabled={pending}
        className="bg-black text-white rounded px-4 py-2"
      >
        Add
      </button>
    </form>
  );
}

If you tried to pass { title, done: false } into orpc.tasks.create, TypeScript would reject it before you ran anything, because createTaskInput only accepts title. That's the input schema doing its job on the client side, not just the server side.

Watching the compiler catch a breaking change

This is the part worth actually trying. Open lib/schemas/task.ts and rename done to completed:

export const taskSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(200),
  completed: z.boolean(),
});

Save the file. NewTaskForm now shows a compile error on task.done: Property 'done' does not exist on type '{ id: string; title: string; completed: boolean }'. lib/db/tasks.ts errors too, since createTask still builds an object with a done key. Nothing runs until every one of those is fixed.

That's the whole pitch. The schema changed once, in one file, and TypeScript found every place downstream that assumed the old shape. No grep for .done across the codebase, no runtime error days later when a real user hits the stale code path.

Using Claude Code to wire this up

Ask Claude Code to add a new procedure and it follows the pattern once your CLAUDE.md states the convention plainly:

## API Layer

- oRPC procedures live in lib/orpc/router.ts
- Every procedure gets both .input() and .output() with a Zod schema
- Zod schemas for API-facing shapes live in lib/schemas/
- Route handler is mounted at app/rpc/[[...rest]]/route.ts, don't create new route files per endpoint

With that in place, "add a procedure to mark a task done" produces a new entry in the same router object, a schema update if needed, and a handler that matches the existing style, instead of a fresh ad-hoc API route with its own hand-written types.

The $29 Code Kit builds on exactly this pattern. It's a harness on top of Claude Code, not a separate framework, and the API layer it generates is oRPC with Zod by default, type-safe from your database queries through to the component that renders the data. You still need a paid Anthropic plan to run Claude Code itself. What the harness adds is the convention already wired in, so every new feature it builds gets this same guarantee without you having to ask for it each time.

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →

Posted by @speedy_devv

Continue in Workflow

  • Commerce agentique : comment construire une app que les agents IA peuvent payer
    Un guide en français simple du commerce agentique en 2026 : ce que font x402, ACP et le Machine Payments Protocol, plus un pas-à-pas d'un week-end pour livrer une API payante que les agents IA peuvent acheter.
  • Bonnes pratiques Claude Code
    Cinq habitudes séparent les ingénieurs qui livrent avec Claude Code : les PRDs, les règles CLAUDE.md modulaires, les slash commands personnalisés, les resets /clear, et un état d'esprit d'évolution du système.
  • Le mode auto de Claude Code
    Un second modèle Sonnet examine chaque appel d'outil Claude Code avant qu'il s'exécute. Ce que le mode auto bloque, ce qu'il autorise, et les règles d'autorisation qu'il place dans tes paramètres.
  • Channels, Routines, Teleport, Dispatch
    Les quatre fonctionnalités Claude Code livrées par Anthropic en mars et avril 2026 qui transforment le CLI en une couche de coordination orientée événements, entre téléphone, web et desktop.
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • Adding Authentication With Claude Code (Supabase Auth)
    Add email/password signup, Google OAuth, magic links, protected routes, and session handling to a Next.js 16 app using Claude Code and Supabase Auth.

More from Handbook

  • Principes de base de l'agent
    Cinq façons de construire des agents spécialisés dans le code Claude : Sous-agents de tâches, .claude/agents YAML, commandes slash personnalisées, personas CLAUDE.md, et invites de perspective.
  • L'ingénierie du harness agent
    Le harness, c'est toutes les couches autour de ton agent IA sauf le modèle lui-même. Découvre les cinq leviers de contrôle, le paradoxe des contraintes, et pourquoi le design du harness détermine les performances de l'agent bien plus que le modèle.
  • Patterns d'agents
    Orchestrateur, fan-out, chaîne de validation, routage par spécialiste, raffinement progressif, et watchdog. Six formes d'orchestration pour câbler des sub-agents Claude Code.
  • Meilleures pratiques des équipes d'agents
    Patterns éprouvés pour les équipes d'agents Claude Code. Prompts de création riches en contexte, tâches bien calibrées, propriété des fichiers, mode délégué, et correctifs v2.1.33-v2.1.45.

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →

Adding Transactional Email With Claude Code (Resend + React Email)

How to build welcome, receipt, and password reset emails in a Next.js 16 app using Resend and React Email, with Claude Code writing the templates and the send logic.

Commerce agentique : comment construire une app que les agents IA peuvent payer

Un guide en français simple du commerce agentique en 2026 : ce que font x402, ACP et le Machine Payments Protocol, plus un pas-à-pas d'un week-end pour livrer une API payante que les agents IA peuvent acheter.

On this page

The bug this setup prevents
Installing oRPC and Zod
Defining the schema
Defining an oRPC procedure
Mounting the router as a Route Handler
Calling it from a Server Component
Calling it from a Client Component
Watching the compiler catch a breaking change
Using Claude Code to wire this up

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →