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
Failed Payment RecoveryLocalization and i18nPDF InvoicesReferral ProgramTwo-Factor AuthNext.js DevTools MCPNext.js Agent SetupSubscription BillingMulti-Tenant SaaSAI Chat FeatureRate LimitingStripe WebhooksSemantic SearchRealtime UpdatesDrip Email Sequencesv2.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 CodeSupabase DatabaseVercel DeepsecTest-Driven DevelopmentConstruire un MVP SaaS avec Claude CodeAjouter l'authentification avec Claude Code (Supabase Auth)Ajouter les emails transactionnels avec Claude Code (Resend + React Email)Construire une API type-safe avec Claude Code (oRPC + Zod)File UploadsBackground Jobs (Inngest)Admin DashboardFull-Text SearchClaude Agent SDKKiro Migration GuideClaude Research AgentLeave Grok BuildRoute Subagent ModelsClaude Monorepo SetupCI Repair AgentVisual Regression TestsAgent Cost DashboardCursor Migration GuideCommerce agentique : comment construire une app que les agents IA peuvent payer1M ContextUser API KeysAudit LogsCSV Import PipelineDatabase MigrationsProduction Error TrackingFeature FlagsGitHub ActionsHeadless ModeMax Plan vs APICaching and RevalidationIn-App NotificationsOutbound WebhooksPrompt CachingRoles & PermissionsMarketplace PaymentsUsage-Based BillingCombien coûte la création d'un SaaS avec Claude Code en 2026Parallel AI AgentsCoding Agent Injection
speedy_devvkoen_salo
Blog/Handbook/Workflow/Type-Safe API

Construire une API type-safe avec Claude Code (oRPC + Zod)

Comment construire une couche API entièrement type-safe dans Next.js 16 avec oRPC et Zod, avec Claude Code, pour que les changements de schéma cassent le build plutôt que la prod.

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

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

La version d'une API type-safe qui compte vraiment n'est pas un diagramme, c'est ceci : tu renommes un champ dans ton schéma Zod, et le composant client qui le lit vire au rouge dans ton éditeur avant même que tu aies sauvegardé le fichier. Pas de collection Postman à mettre à jour. Pas de 500 runtime en prod trois jours plus tard. L'erreur apparaît au moment exact où tu as cassé quelque chose, dans le fichier exact que tu dois corriger.

C'est ce que oRPC plus Zod te donnent dans une app Next.js 16, et Claude Code est un moyen rapide de le brancher correctement au premier passage.

Le bug que ce setup empêche

Le mode d'échec courant dans une route API Next.js typique ressemble à ça : tu définis un route handler qui renvoie { id, title, done }. Ailleurs, un composant déstructure task.completed parce que quelqu'un a renommé le champ sur le serveur il y a six semaines et que personne n'a mis à jour le client. TypeScript ne peut pas l'attraper, parce que fetch() renvoie any (ou un type que tu as écrit à la main et auquel tu fais désormais aveuglément confiance). Le bug part en prod. Tu l'apprends d'une alerte Sentry, pas d'un compilateur.

« Type-safe de la base de données au frontend » veut dire qu'il y a exactement une source de vérité pour la forme de tes données, un schéma Zod, et que ton handler serveur comme ton appel client sont typés à partir de ce même schéma. Change le schéma, et chaque endroit où cette forme circule s'allume d'une erreur de type jusqu'à ce que tu le corriges.

Installer oRPC et Zod

oRPC se livre en quelques petits packages : un package serveur pour définir les procédures et les monter comme handler, et un package client pour les appeler avec une inférence complète. Installe les deux à côté de Zod.

npm install @orpc/server @orpc/client zod

Rien d'autre n'est requis. Pas d'étape de génération de code, pas de CLI à lancer après chaque changement de schéma. Les types viennent de TypeScript qui infère à travers le schéma Zod, pas d'un artefact de build que tu dois te rappeler de régénérer.

Définir le schéma

Commence par la forme de la chose que ton API renvoie. C'est une petite API de liste de tâches, un schéma d'objet Zod pour une tâche, et un schéma dérivé pour ce qu'une requête de « création » accepte (juste le titre, puisque id et done sont fixés côté serveur).

// 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 fait le vrai travail ici. Task n'est pas un type que tu maintiens à la main, il est dérivé du schéma, il ne peut donc jamais dériver et se désynchroniser du validateur runtime. Si tu ajoutes un champ à taskSchema, Task le récupère automatiquement.

Pour le tutoriel, la « base de données » est un tableau en mémoire. Remplace-le par de vraies requêtes Supabase ou Postgres dans une app de prod, le reste du setup ne change pas.

// 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;
}

Définir une procédure oRPC

Une procédure dans oRPC se construit avec os, un builder chaînable de @orpc/server. Tu y attaches un schéma d'input, un schéma d'output et une fonction handler. Les deux appels de schéma sont optionnels, mais attacher les deux est ce qui te donne la validation à l'entrée et une forme typée et vérifiée à la sortie.

// 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;

Deux choses valent le coup d'être remarquées. D'abord, input dans le handler create est déjà typé comme { title: string }, pas de cast, pas d'interface manuelle. Ensuite, si createTask renvoyait quelque chose qui ne correspond pas à taskSchema, TypeScript le signale directement dans le handler, avant même que le code ne tourne. Le schéma d'output est un contrat sur ton propre code serveur, pas seulement sur le client.

Monter le router comme Route Handler

oRPC expose un handler RPC qui parle du simple fetch Request/Response, ce qui se calque directement sur un Route Handler de Next.js 16. Monte-le sur un segment catch-all pour que chaque procédure du router soit joignable sous un seul préfixe.

// 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;

Chaque verbe HTTP pointe vers la même fonction handleRequest parce que le handler RPC détermine quelle procédure appeler à partir de la requête elle-même. Tu n'écris pas un fichier séparé par endpoint, et tu ne mappes pas les URL vers les fonctions à la main. Ajouter une nouvelle procédure à router dans lib/orpc/router.ts est tout le diff.

L'appeler depuis un Server Component

Dans un Server Component, il n'y a aucune raison de faire un aller-retour HTTP pour appeler ton propre serveur. Le client côté serveur d'oRPC (createRouterClient) appelle la fonction handler de la procédure directement, en process, tout en gardant exactement la même interface typée que le client réseau.

// 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: {},
});

L'import server-only est un garde-fou, il lève une erreur de build si ce fichier est un jour tiré dans du code client par accident. Utilise-le dans une 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 est Task[], inféré jusqu'au bout depuis taskSchema. Pas de type de retour manuel sur TasksPage, pas d'any qui se glisse depuis un appel fetch non typé.

L'appeler depuis un Client Component

Le navigateur ne peut pas appeler tes fonctions serveur en process, donc le client côté navigateur passe par le réseau, en utilisant le lien RPC pointé vers la route que tu as montée plus tôt. L'interface est identique à celle du client côté serveur, mêmes noms de méthodes, mêmes formes d'arguments, mêmes types de retour inférés.

// 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);

Utilise-le dans un formulaire. title est une simple string venant de useState, et la valeur de retour de orpc.tasks.create(...) est une Task entièrement typée, pas une Response que tu dois .json() et caster.

// 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>
  );
}

Si tu essayais de passer { title, done: false } dans orpc.tasks.create, TypeScript le rejetterait avant que tu ne lances quoi que ce soit, parce que createTaskInput n'accepte que title. C'est le schéma d'input qui fait son boulot côté client, pas seulement côté serveur.

Voir le compilateur attraper un changement cassant

C'est la partie qui vaut vraiment le coup d'essayer. Ouvre lib/schemas/task.ts et renomme done en completed :

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

Sauvegarde le fichier. NewTaskForm affiche maintenant une erreur de compilation sur task.done : Property 'done' does not exist on type '{ id: string; title: string; completed: boolean }'. lib/db/tasks.ts erreur aussi, puisque createTask construit encore un objet avec une clé done. Rien ne tourne tant que chacune de ces erreurs n'est pas corrigée.

C'est tout l'argument. Le schéma a changé une fois, dans un seul fichier, et TypeScript a trouvé chaque endroit en aval qui supposait l'ancienne forme. Pas de grep sur .done dans toute la codebase, pas d'erreur runtime des jours plus tard quand un vrai utilisateur atteint le chemin de code périmé.

Utiliser Claude Code pour brancher ça

Demande à Claude Code d'ajouter une nouvelle procédure et il suit le pattern une fois que ton CLAUDE.md énonce la convention clairement :

## 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

Avec ça en place, « ajoute une procédure pour marquer une tâche comme faite » produit une nouvelle entrée dans le même objet router, une mise à jour de schéma si besoin, et un handler qui colle au style existant, au lieu d'une nouvelle route API ad hoc avec ses propres types écrits à la main.

Le Code Kit à $29 s'appuie exactement sur ce pattern. C'est un moteur posé sur Claude Code, pas un framework à part, et la couche API qu'il génère est oRPC avec Zod par défaut, type-safe depuis tes requêtes de base de données jusqu'au composant qui affiche les données. Tu as toujours besoin d'un plan Anthropic payant pour faire tourner Claude Code lui-même. Ce que le moteur ajoute, c'est la convention déjà branchée, pour que chaque nouvelle fonctionnalité qu'il construit ait cette même garantie sans que tu aies à la demander à chaque fois.

Posté par @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.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Best SaaS Boilerplate 2026: The Honest Comparison
    An honest 2026 roundup of the best SaaS boilerplates and starter kits (ShipFast, Makerkit, Supastarter, SaaS Pegasus, Divjoy, open source), with real pricing, stacks, and the trade-off nobody mentions: a boilerplate still leaves you coding.
  • Changelog Claude Code
    Notes de version par version pour Claude Code, du bêta v0.2 à mars 2026. Bare mode, relais de permissions Channels, corrections OAuth, et chaque breaking change.
  • What It Really Costs to Build a SaaS MVP in 2026
    A buyer's cost breakdown for building a SaaS MVP in 2026. Real freelancer, agency, no-code, AI-tool, and DIY numbers with citations, plus where the money actually goes.
  • Templates de prompts qui livrent du code
    Dix recettes de prompts qui livrent du code : scaffolding full-stack, APIs, schémas, tests, refactors, debugging, reviews et CI. Chacun avec les modes d'échec à éviter.

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvkoen_salo

Ajouter les emails transactionnels avec Claude Code (Resend + React Email)

Comment construire des emails de bienvenue, de reçu et de réinitialisation de mot de passe dans une app Next.js 16 avec Resend et React Email, en laissant Claude Code écrire les templates et la logique d'envoi.

File Uploads

Build secure user file and image uploads end to end with Claude Code: private Supabase Storage buckets, per-user RLS, signed URLs, and an upload UI wired through a type-safe oRPC API.

On this page

Le bug que ce setup empêche
Installer oRPC et Zod
Définir le schéma
Définir une procédure oRPC
Monter le router comme Route Handler
L'appeler depuis un Server Component
L'appeler depuis un Client Component
Voir le compilateur attraper un changement cassant
Utiliser Claude Code pour brancher ça

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →