Use Cases (Mis à jour: 02/06/2026)

Créer une API type-safe avec tRPC et Claude Code

Créer une API type-safe avec Claude Code et tRPC: Next.js, Zod, autorisation, cache invalidation et prompts de revue.

Créer une API type-safe avec tRPC et Claude Code

Ce que tRPC et Claude Code apportent ensemble

tRPC permet d’utiliser le router TypeScript côté serveur comme contrat partagé avec le client. Le front-end n’a pas besoin d’un fichier OpenAPI séparé ni d’un SDK écrit à la main: il importe le type AppRouter et reçoit l’autocomplétion pour les inputs, les outputs et les erreurs. Pour une équipe qui modifie souvent les écrans et les endpoints, cela réduit fortement les écarts entre API et UI.

Claude Code devient utile quand il lit le projet réel au lieu de produire seulement du boilerplate. Il peut tenir compte de l’authentification existante, de l’organisation des dossiers, de la couche de données, des conventions de React Query et des règles de nommage. Il peut alors générer des routers, des procedures, des schémas Zod, des contrôles d’autorisation et des invalidations de cache cohérents. Mais la sécurité de type ne remplace pas les règles métier: une mutation peut compiler et rester dangereuse si elle oublie de vérifier le propriétaire d’une ressource.

L’exemple utilise Next.js App Router avec une petite API de gestion de projets. Le stockage est un Map en mémoire pour garder le code copiable. Dans une vraie application, remplacez cette partie par Prisma, Drizzle, Supabase ou votre couche de données, puis remplacez aussi la session de démonstration par votre authentification réelle.

Cas d’usage concrets

tRPC convient surtout lorsque le serveur et le client évoluent dans le même codebase TypeScript. Pour une API publique destinée à des partenaires externes, OpenAPI peut être préférable. Pour un back-office, un outil interne ou un BFF léger, tRPC accélère souvent les itérations.

Cas d’usageCe que tRPC apporteCe que Claude Code peut générerRisque principal
CRUD d’administrationInputs et outputs partagés pour liste, création, édition, suppressionRouter, schémas Zod, mutations, invalidationMasquer un bouton sans autorisation serveur
Outils internesChangements rapides sans SDK séparéProcedures depuis les modèles existantsContext trop volumineux
FormulairesValidation runtime des emails, longueurs et enumsMessages d’erreur et anti double-submitFaire confiance aux types statiques uniquement
BFF légerRéponse adaptée à l’écranMapping d’APIs externesCache et refetch mal définis
flowchart LR
  UI["React component"]
  Client["tRPC React client"]
  Router["AppRouter"]
  Procedure["protected/admin procedure"]
  Zod["Zod validation"]
  Store["DB or store"]
  Review["Claude Code review"]

  UI --> Client --> Router --> Procedure --> Zod --> Store
  Router --> Review
  Review --> Procedure

Installation et structure

Installez les dépendances de base. Si React Query ou Zod sont déjà présents, vérifiez les versions avant de demander à Claude Code d’écrire le router.

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod superjson

La structure suivante garde le code serveur sous src/server, l’adaptateur HTTP dans l’App Router, et le provider React dans src/trpc.

src/
  app/api/trpc/[trpc]/route.ts
  app/projects/project-list.tsx
  server/trpc.ts
  server/routers/_app.ts
  server/routers/project.ts
  trpc/client.tsx

Donnez cette frontière à Claude Code dès le début: les fichiers serveur ne doivent pas importer de composants client, les composants client ne doivent pas accéder directement au store, et les types partagés doivent passer par le router tRPC plutôt que par un DTO parallèle.

Context et procedures d’autorisation

Le context contient les données disponibles pour chaque request. Gardez-le petit: session, équipe, rôle et accès base de données suffisent souvent. L’exemple ci-dessous lit des headers pour la démo locale.

// src/server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";

type Role = "admin" | "member";
type Session = { userId: string; teamId: string; role: Role };
export type Context = { session: Session | null };

export async function createContext({
  headers,
}: {
  headers: Headers;
}): Promise<Context> {
  const roleHeader = headers.get("x-user-role");
  const role: Role =
    roleHeader === "admin"
      ? "admin"
      : roleHeader === "member"
        ? "member"
        : process.env.NODE_ENV === "production"
          ? "member"
          : "admin";

  return {
    session: {
      userId: headers.get("x-user-id") ?? "demo-user",
      teamId: headers.get("x-team-id") ?? "demo-team",
      role,
    },
  };
}

const t = initTRPC.context<Context>().create({ transformer: superjson });

export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;

const requireUser = t.middleware(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: "Login is required." });
  }
  return next({ ctx: { session: ctx.session } });
});

export const protectedProcedure = t.procedure.use(requireUser);

export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
  if (ctx.session.role !== "admin") {
    throw new TRPCError({ code: "FORBIDDEN", message: "Admin role is required." });
  }
  return next();
});

Séparer protectedProcedure et adminProcedure facilite la revue. Une mutation destructrice qui utilise le mauvais niveau d’autorisation devient visible immédiatement dans le diff.

Router avec validation Zod

TypeScript vérifie le code au build, mais pas le JSON envoyé par un navigateur, un script ou un client compromis. Zod valide ces données au runtime, à la frontière de la procedure.

// src/server/routers/project.ts
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";

type ProjectStatus = "todo" | "doing" | "done";
type Project = {
  id: string;
  teamId: string;
  title: string;
  ownerEmail: string;
  status: ProjectStatus;
  createdAt: string;
};

const projects = new Map<string, Project>();
const projectStatus = z.enum(["todo", "doing", "done"]);

const listProjectsInput = z
  .object({
    status: projectStatus.optional(),
    query: z.string().trim().max(60).optional(),
    limit: z.number().int().min(1).max(50).default(20),
  })
  .default({ limit: 20 });

const createProjectInput = z.object({
  title: z.string().trim().min(2).max(80),
  ownerEmail: z.string().email(),
});

export const projectRouter = createTRPCRouter({
  list: protectedProcedure.input(listProjectsInput).query(({ ctx, input }) => {
    return [...projects.values()]
      .filter((project) => project.teamId === ctx.session.teamId)
      .filter((project) => !input.status || project.status === input.status)
      .filter((project) => !input.query || project.title.includes(input.query))
      .slice(0, input.limit);
  }),

  create: adminProcedure.input(createProjectInput).mutation(({ ctx, input }) => {
    const project: Project = {
      id: crypto.randomUUID(),
      teamId: ctx.session.teamId,
      title: input.title,
      ownerEmail: input.ownerEmail,
      status: "todo",
      createdAt: new Date().toISOString(),
    };
    projects.set(project.id, project);
    return project;
  }),

  updateStatus: protectedProcedure
    .input(z.object({ id: z.string().uuid(), status: projectStatus }))
    .mutation(({ ctx, input }) => {
      const project = projects.get(input.id);
      if (!project || project.teamId !== ctx.session.teamId) {
        throw new TRPCError({ code: "NOT_FOUND" });
      }
      const nextProject = { ...project, status: input.status };
      projects.set(input.id, nextProject);
      return nextProject;
    }),
});

La vérification teamId est volontairement proche de l’opération d’écriture. Un UUID valide ne suffit pas: il faut prouver que la ressource appartient à l’équipe courante avant de la modifier.

Connexion à App Router et au client React

Composez le router principal puis exposez-le avec un Route Handler.

// src/server/routers/_app.ts
import { createTRPCRouter } from "../trpc";
import { projectRouter } from "./project";

export const appRouter = createTRPCRouter({
  project: projectRouter,
});

export type AppRouter = typeof appRouter;
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/trpc";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => createContext({ headers: req.headers }),
  });

export { handler as GET, handler as POST };
// src/trpc/client.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { useState, type ReactNode } from "react";
import superjson from "superjson";
import type { AppRouter } from "@/server/routers/_app";

export const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [httpBatchLink({ url: "/api/trpc" })],
    }),
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}
// src/app/projects/project-list.tsx
"use client";

import { useState } from "react";
import { trpc } from "@/trpc/client";

export function ProjectList() {
  const utils = trpc.useUtils();
  const [title, setTitle] = useState("");
  const projects = trpc.project.list.useQuery({ limit: 20 });
  const createProject = trpc.project.create.useMutation({
    onSuccess: async () => {
      setTitle("");
      await utils.project.list.invalidate();
    },
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        createProject.mutate({ title, ownerEmail: "owner@example.com" });
      }}
    >
      <input value={title} onChange={(event) => setTitle(event.target.value)} />
      <button type="submit">Add</button>
      <pre>{JSON.stringify(projects.data, null, 2)}</pre>
    </form>
  );
}

L’appel à utils.project.list.invalidate() après la mutation évite une liste obsolète. Définir cette règle dès l’implémentation est plus fiable que corriger plus tard des incohérences d’interface.

Prompt de revue pour Claude Code

Revois cette implémentation Next.js App Router + tRPC.
Vérifie:
1. L'usage de publicProcedure, protectedProcedure et adminProcedure.
2. La validation Zod de chaque input externe.
3. Le contrôle de teamId et userId avant lecture ou écriture.
4. L'invalidation des queries touchées par les mutations.
5. Le découpage des routers par domaine.
6. Un context limité aux dépendances par request.
7. L'absence de champs secrets dans les retours client.

Retourne la sévérité, le fichier, le problème, la correction et les tests à ajouter dans un tableau.

Ce prompt peut vivre dans CLAUDE.md ou dans un template de pull request. Pour les changements d’autorisation, demandez d’abord un diff et une analyse du risque, puis appliquez seulement après revue humaine.

Pièges courants

Le premier piège est le context obèse: tout y mettre rend les tests fragiles. Le deuxième est l’autorisation uniquement côté UI: cacher un bouton ne protège pas l’endpoint. Le troisième est l’absence de validation runtime: TypeScript ne valide pas les requêtes réelles. Le quatrième est le router monolithique: un seul fichier devient vite impossible à relire. Le cinquième est la confusion de cache: une mutation peut toucher liste, détail, badge et tableau de bord, donc les invalidations doivent être listées avant le code.

Références et accompagnement

Avant d’implémenter, consultez les docs tRPC sur les routers, les procedures et l’intégration React Query. Pour la validation, utilisez la documentation Zod. Pour l’endpoint Next.js, vérifiez les Route Handlers.

Pour continuer, lisez le guide de validation Zod avec Claude Code et les conseils TypeScript avec Claude Code. ClaudeCodeLab peut aussi aider via la formation et consultation Claude Code pour revoir vos routers, vos droits et vos prompts sur un vrai dépôt.

Résultat après essai

Sur un prototype de back-office, la meilleure décision a été de créer protectedProcedure et adminProcedure avant de générer le CRUD. Claude Code pouvait alors signaler rapidement les mutations qui demandaient un niveau d’accès plus strict. Les schémas Zod placés près des procedures ont aussi rendu les changements de formulaires plus lisibles. En revanche, dès que le context grossissait, les tests devenaient pénibles; la version retenue garde donc le context étroit et isole les opérations métier dans des fonctions séparées.

#Claude Code #tRPC #TypeScript #API #type safety
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.