Build This Now
Build This Now
O que é o Código Claude?Instalar o Claude CodeInstalador Nativo do Claude CodeO Teu Primeiro Projeto com Claude Code
Claude Code v2.1.122 Release NotesClaude Code Dynamic Workflows: Como Orquestrar 1.000 Subagentes Num Codebase RealMelhores Práticas do Claude CodeBoas Práticas para o Claude Opus 4.7Claude Code num VPSIntegração GitRevisão de Código com ClaudeWorktrees no Claude CodeControle Remoto do Claude CodeChannels do Claude CodeChannels, Routines, Teleport, DispatchTarefas Agendadas no Claude CodePermissões do Claude CodeModo Auto do Claude CodeAdicionar Pagamentos Stripe Com o Claude CodeFeedback LoopsFluxos de Trabalho com TodosTarefas no Claude CodeTemplates de ProjetoPreços e Consumo de Tokens no Claude CodePreços do Claude Code: O Que Vais Mesmo PagarClaude Code Ultra ReviewConstruir Uma App Next.js Com o 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)Comércio Agêntico: Como Construir uma App Que Agentes de IA Podem PagarClaude 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 OnQuanto Custa Construir um SaaS com Claude Code em 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.

Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.

Veja o que construímos para empresas →
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.


Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.

Veja o que construímos para empresas →

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.

Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.

Veja o que construímos para empresas →

Posted by @speedy_devv

Continue in Workflow

  • Comércio Agêntico: Como Construir uma App Que Agentes de IA Podem Pagar
    Um guia em português simples sobre comércio agêntico em 2026: o que fazem o x402, o ACP e o Machine Payments Protocol, mais um passo a passo de fim de semana para lançar uma API paga que agentes de IA podem comprar.
  • Melhores Práticas do Claude Code
    Cinco hábitos separam os engenheiros que entregam com Claude Code: PRDs, regras modulares em CLAUDE.md, slash commands personalizados, resets com /clear e uma mentalidade de evolução do sistema.
  • Modo Auto do Claude Code
    Um segundo modelo Sonnet revê cada chamada de ferramenta do Claude Code antes de ser executada. O que o modo auto bloqueia, o que permite e as regras de permissão que cria nas tuas definições.
  • Channels, Routines, Teleport, Dispatch
    As quatro funcionalidades de Claude Code que a Anthropic lançou em março e abril de 2026 e que transformam a CLI numa camada de coordenação orientada a eventos entre telemóvel, web e 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

  • Fundamentos do agente
    Cinco maneiras de criar agentes especializados no Código Claude: Sub-agentes de tarefas, .claude/agents YAML, comandos de barra personalizados, personas CLAUDE.md e prompts de perspetiva.
  • Engenharia de Harness para Agentes
    O harness é cada camada ao redor do seu agente de IA, exceto o modelo em si. Aprenda os cinco pontos de controle, o paradoxo das restrições, e por que o design do harness determina o desempenho do agente mais do que o modelo.
  • Padrões de Agentes
    Orchestrator, fan-out, cadeia de validação, routing especializado, refinamento progressivo e watchdog. Seis formas de orquestração para ligar sub-agentes no Claude Code.
  • Boas Práticas para Equipas de Agentes
    Padrões testados em produção para Equipas de Agentes Claude Code. Prompts de criação ricos em contexto, tarefas bem dimensionadas, posse de ficheiros, modo delegado, e correções das versões v2.1.33-v2.1.45.

Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.

Veja o que construímos para empresas →

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.

Comércio Agêntico: Como Construir uma App Que Agentes de IA Podem Pagar

Um guia em português simples sobre comércio agêntico em 2026: o que fazem o x402, o ACP e o Machine Payments Protocol, mais um passo a passo de fim de semana para lançar uma API paga que agentes de IA podem comprar.

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

Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.

Veja o que construímos para empresas →