tRPC v11 : une API full type-safe sans code generation
Zéro schéma GraphQL, zéro codegen, zéro runtime overhead — comment tRPC v11 change la façon de construire des API pour les SaaS Next.js.
Le problème avec REST et GraphQL
REST : vous écrivez votre API, puis vous écrivez les types côté client. Manuellement. À chaque changement. Et quand les types driftent... bug silencieux en production.
GraphQL : type safety, oui. Mais au prix d'un schéma à maintenir, un codegen à lancer, et un runtime qui parse chaque requête.
tRPC : vous écrivez votre API en TypeScript, et le client a les types automatiquement. Pas de codegen, pas de schéma, pas de runtime overhead.
Setup dans un SaaS Next.js
Le router tRPC
// src/server/api/routers/invoice.ts
import { z } from "zod";
import { createTRPCRouter, staffProcedure } from "~/server/api/trpc";
export const invoiceRouter = createTRPCRouter({
getAll: staffProcedure.query(async ({ ctx }) => {
return ctx.orgDb.invoice.findMany({
orderBy: { createdAt: "desc" },
include: { client: true },
});
}),
create: staffProcedure
.input(
z.object({
clientId: z.string(),
items: z.array(
z.object({
description: z.string(),
quantity: z.number().positive(),
unitPrice: z.number().positive(),
}),
),
}),
)
.mutation(async ({ ctx, input }) => {
return ctx.db.invoice.create({
data: {
organizationId: ctx.session.user.organizationId,
clientId: input.clientId,
items: { create: input.items },
},
});
}),
});Côté client — zéro configuration
"use client";
import { api } from "~/trpc/react";
export function InvoiceList() {
const { data, isLoading } = api.invoice.getAll.useQuery();
if (isLoading) return <Skeleton />;
return (
<div>
{data?.map((invoice) => (
// invoice est typé automatiquement — autocomplétion complète
<InvoiceCard key={invoice.id} invoice={invoice} />
))}
</div>
);
}Changez le type de retour côté serveur → TypeScript vous signale immédiatement les erreurs côté client. Avant même de lancer l'app.
La hiérarchie des procédures
tRPC v11 permet de chaîner des middlewares pour créer des niveaux d'accès :
// Du moins restrictif au plus restrictif
export const publicProcedure = t.procedure;
export const protectedProcedure = publicProcedure.use(enforceAuth);
export const staffProcedure = protectedProcedure.use(enforceStaffRole);
export const adminProcedure = protectedProcedure.use(enforceAdminRole);
// Permission granulaire
export const requirePermission = (perm: string) =>
staffProcedure.use(({ ctx, next }) => {
if (!hasPermission(ctx.session.user.role, perm)) {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});Chaque router choisit le bon niveau. Une query publique ? publicProcedure. Un CRUD admin ? requirePermission("invoices:write").
Validation avec Zod
tRPC s'intègre nativement avec Zod pour la validation d'input :
.input(
z.object({
search: z.string().optional(),
page: z.number().int().positive().default(1),
perPage: z.number().int().min(1).max(100).default(20),
})
)Le type de input dans votre handler est inféré automatiquement depuis le schéma Zod. Un seul endroit pour la validation ET les types.
Mutations optimistes
const utils = api.useUtils();
const createInvoice = api.invoice.create.useMutation({
onSuccess: () => {
// Invalider le cache pour refetch
utils.invoice.getAll.invalidate();
},
});Pourquoi pas GraphQL ?
| Critère | tRPC | GraphQL |
|---|---|---|
| Codegen | Non | Oui |
| Runtime overhead | Zéro | Parsing + résolution |
| Setup | 10 min | 30 min + tooling |
| Type safety | Full | Full (avec codegen) |
| Cas d'usage | SaaS monorepo | API publique multi-client |
Règle simple : si votre API est consommée uniquement par votre frontend (SaaS), prenez tRPC. Si vous avez des clients externes (mobile tiers, partenaires), prenez GraphQL.
Conclusion
tRPC v11 élimine toute une catégorie de bugs — les types driftés entre client et serveur. Dans un SaaS où la vélocité compte, c'est un gain de productivité énorme.