Ce qu'il faut retenir
tRPC élimine la couche de contrat API manuel entre frontend et backend Next.js : les types TypeScript sont partagés de bout en bout, sans génération de code, sans OpenAPI spec. Un changement de signature côté serveur devient immédiatement une erreur de compilation côté client.
Le setup avec Next.js App Router nécessite une configuration spécifique — tRPC s'intègre via des Route Handlers et un client React Query côté client, tandis que les Server Components peuvent appeler les procedures directement sans HTTP.
tRPC est le bon choix pour les applications full-stack TypeScript où frontend et backend vivent dans le même repo ou monorepo. Il n'est pas adapté aux APIs publiques consommées par des tiers, ni aux équipes avec des stacks frontend/backend hétérogènes.
tRPC avec Next.js App Router 2026 : type-safety E2E guide complet
Tribune de Chokri Siala, CTO Nehos — setup App Router, routers, mutations, middleware authentification, validation Zod, gestion d'erreurs, tests et les cas où tRPC n'est pas le bon choix.
Adapté à toute taille de structure
#Pourquoi tRPC change la façon de construire des APIs en 2026
La question qui revenait dans toutes nos revues de code en 2024 : pourquoi continuons-nous à écrire du code pour décrire des APIs que nous allons consommer nous-mêmes ? REST avec OpenAPI génère des types. GraphQL génère des types. Les deux nécessitent une étape de génération, une toolchain supplémentaire, un contrat à maintenir synchronisé.
tRPC résout ce problème d'une façon différente : il supprime le concept d'API au sens traditionnel. Le serveur expose des procedures TypeScript — des fonctions avec leurs types d'entrée et de sortie. Le client consomme ces procedures comme si c'était des fonctions locales. Il n'y a pas de contrat à maintenir parce qu'il n'y a qu'une seule source de vérité : le code TypeScript.
En 2026, cette approche est mature. tRPC v11 est stable, son intégration avec Next.js App Router est documentée et testée en production sur des centaines de projets. Le State of JS 2025 classe tRPC parmi les outils avec le taux de satisfaction le plus élevé dans la catégorie API layers — 92% des développeurs qui l'ont utilisé déclarent vouloir continuer à l'utiliser.
#Le problème que tRPC résout vraiment
Prenons un exemple concret. Votre équipe ajoute un champ companySize à l'endpoint /api/users/:id. Avec REST classique, ce changement nécessite :
- Modifier le handler API côté serveur
- Mettre à jour le type
Userdans le fichier de types partagés (si vous en avez un) - Mettre à jour la spec OpenAPI (si elle est maintenue)
- Regénérer les types client (si vous utilisez openapi-typescript)
- Mettre à jour le code frontend qui consomme l'endpoint
Avec tRPC, ce changement se réduit à :
- Modifier la procedure tRPC côté serveur — ajouter
companySizeau schéma Zod et à la valeur retournée - Le code frontend qui n'utilise pas
companySizecontinue de compiler. Le code qui en a besoin peut y accéder immédiatement avec autocomplétion.
Aucune génération de code. Aucun contrat intermédiaire. Le compilateur TypeScript est le contrat.
#Ce que signifie concrètement "type-safety E2E"
La "type-safety end-to-end" n'est pas un argument marketing — c'est une propriété vérifiable. Avec tRPC, si vous modifiez le type de retour d'une procedure sur le serveur, toutes les utilisations côté client qui ne correspondent plus au nouveau type généreront des erreurs de compilation. Cette garantie couvre :
- Les paramètres d'entrée des queries et mutations
- Les types de retour
- Les codes d'erreur spécifiques
- Les types des subscriptions WebSocket
Cette propriété transforme les revues de code : les changements d'API sont détectables statiquement, pas seulement en runtime.
#tRPC vs REST vs GraphQL : comparatif pour projets Next.js B2B
Chaque approche a un domaine d'application naturel. Le choix dépend moins de la qualité intrinsèque de chaque solution que de votre contexte.
#REST + OpenAPI : le standard interopérable
REST reste le choix par défaut pour les APIs B2B qui doivent être consommées par des tiers — partenaires, intégrateurs, applications mobiles indépendantes. Le contrat OpenAPI est un standard d'industrie : n'importe quel écosystème peut générer un client typé depuis une spec OpenAPI.
Pour quand : API publique, API consommée par des équipes non-TypeScript, équipe frontend et backend séparées dans des repos distincts, besoin de versionning d'API explicite (v1, v2).
Limites : overhead de maintenance du contrat, étape de génération de code, risque de désynchronisation spec/implémentation sans CI strict.
#GraphQL : la flexibilité au prix de la complexité
GraphQL brille pour les APIs avec des relations de données complexes et des consommateurs aux besoins hétérogènes — le frontend peut requêter exactement ce dont il a besoin, ni plus ni moins. C'est l'approche de référence pour les plateformes avec plusieurs clients (web, mobile, partenaires).
Pour quand : données fortement relationnelles, plusieurs clients avec des besoins de sélection de champs différents, API exposée à des partenaires externes qui bénéficient de la flexibilité des queries.
Limites : complexité de mise en place (schema, resolvers, codegen), N+1 problem à gérer avec DataLoader, courbe d'apprentissage équipe. Sur un projet Next.js full-stack sans clients externes, GraphQL est souvent overengineered.
#tRPC : la productivité maximale sur les projets full-stack TypeScript
Pour quand : application full-stack TypeScript avec frontend et backend dans le même repo ou monorepo, API consommée uniquement par votre propre frontend, équipe 100% TypeScript, projet B2B où la vitesse de livraison est critique.
Limites : non adapté aux APIs publiques (pas de spec OpenAPI native), ne fonctionne que dans des écosystèmes TypeScript, couplage fort entre frontend et backend (ce qui est une feature pour les projets internes, mais une contrainte pour les APIs partagées).
#Tableau comparatif pour Next.js B2B 2026
| Critère | REST + OpenAPI | GraphQL | tRPC |
|---|---|---|---|
| Type-safety E2E | Via codegen | Via codegen | Native |
| Courbe d'apprentissage | Faible | Élevée | Moyenne |
| API publique/tierce | Oui | Oui | Non |
| Vitesse de développement | Moyenne | Faible | Élevée |
| Flexibilité des queries | Fixe | Très élevée | Fixe |
| Maintenance contrat | Élevée | Élevée | Nulle |
| Server Components support | Manuel | Manuel | Natif |
| Abonnements temps réel | Via SSE/WS manuel | Natif | Natif |
#Setup tRPC avec Next.js App Router et React Server Components
Le setup tRPC avec App Router diffère significativement de Next.js Pages Router. L'architecture App Router offre une intégration plus profonde via les Server Components.
#Installation
pnpm add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
pnpm add -D @types/node
#Structure des fichiers recommandée
src/
├── server/
│ └── trpc/
│ ├── init.ts # Initialisation tRPC + context
│ ├── routers/
│ │ ├── _app.ts # Router racine (merge)
│ │ ├── users.ts
│ │ └── projects.ts
│ └── trpc.ts # Helpers exportés (procedure, router)
├── app/
│ └── api/
│ └── trpc/
│ └── [trpc]/
│ └── route.ts # Route Handler Next.js
└── lib/
└── trpc/
├── client.ts # Client tRPC React Query
├── server.ts # Caller tRPC pour Server Components
└── provider.tsx # TRPCReactProvider
#Initialisation du contexte tRPC
// src/server/trpc/init.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { cache } from 'react';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import superjson from 'superjson';
import { ZodError } from 'zod';
export type Context = {
session: Awaited<ReturnType<typeof getServerSession>> | null;
headers: Headers;
};
export const createTRPCContext = cache(async (opts: { headers: Headers }): Promise<Context> => {
const session = await getServerSession(authOptions);
return {
session,
headers: opts.headers,
};
});
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const { router, procedure: publicProcedure, middleware } = t;
export const createCallerFactory = t.createCallerFactory;
#Route Handler Next.js App Router
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/trpc/routers/_app';
import { createTRPCContext } from '@/server/trpc/init';
import { NextRequest } from 'next/server';
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ headers: req.headers }),
onError({ error, path }) {
if (process.env.NODE_ENV === 'development') {
console.error(`tRPC error on ${path}:`, error);
}
},
});
export { handler as GET, handler as POST };
#Caller pour Server Components
Avec App Router, les Server Components peuvent appeler les procedures tRPC directement sans passer par HTTP — une optimisation de performance majeure :
// src/lib/trpc/server.ts
import { createCallerFactory } from '@/server/trpc/init';
import { appRouter } from '@/server/trpc/routers/_app';
import { createTRPCContext } from '@/server/trpc/init';
import { cache } from 'react';
import { headers } from 'next/headers';
const createCaller = createCallerFactory(appRouter);
export const getServerCaller = cache(async () => {
const heads = await headers();
const ctx = await createTRPCContext({ headers: heads });
return createCaller(ctx);
});
Dans un Server Component :
// src/app/dashboard/page.tsx
import { getServerCaller } from '@/lib/trpc/server';
export default async function DashboardPage() {
const trpc = await getServerCaller();
// Appel direct, pas de réseau — même processus
const projects = await trpc.projects.list({ limit: 10 });
return <ProjectsList projects={projects} />;
}
Cette architecture exploite le paradigme des Server Components React 2026 : le data fetching se produit côté serveur, sans roundtrip HTTP, avec le plein support du cache React.
#Définir les routers et procedures : query, mutation, subscription
#Structure d'un router
// src/server/trpc/routers/projects.ts
import { z } from 'zod';
import { router, publicProcedure } from '../init';
import { protectedProcedure } from '../middlewares/auth';
const projectSchema = z.object({
id: z.string().cuid(),
name: z.string().min(2).max(100),
status: z.enum(['draft', 'active', 'archived']),
createdAt: z.date(),
updatedAt: z.date(),
});
const createProjectInput = z.object({
name: z.string().min(2).max(100),
description: z.string().max(500).optional(),
});
export const projectsRouter = router({
// QUERY : lecture de données
list: protectedProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(20),
cursor: z.string().cuid().optional(),
}))
.output(z.object({
items: z.array(projectSchema),
nextCursor: z.string().cuid().optional(),
}))
.query(async ({ ctx, input }) => {
const items = await ctx.db.project.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
where: { userId: ctx.session.user.id },
orderBy: { createdAt: 'desc' },
});
const nextCursor = items.length > input.limit
? items.pop()!.id
: undefined;
return { items, nextCursor };
}),
// MUTATION : écriture de données
create: protectedProcedure
.input(createProjectInput)
.mutation(async ({ ctx, input }) => {
return ctx.db.project.create({
data: {
...input,
userId: ctx.session.user.id,
status: 'draft',
},
});
}),
delete: protectedProcedure
.input(z.object({ id: z.string().cuid() }))
.mutation(async ({ ctx, input }) => {
const project = await ctx.db.project.findUnique({
where: { id: input.id },
});
if (!project || project.userId !== ctx.session.user.id) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Projet introuvable ou accès non autorisé',
});
}
await ctx.db.project.delete({ where: { id: input.id } });
return { success: true };
}),
});
#Router racine avec merge
// src/server/trpc/routers/_app.ts
import { router } from '../init';
import { usersRouter } from './users';
import { projectsRouter } from './projects';
import { analyticsRouter } from './analytics';
export const appRouter = router({
users: usersRouter,
projects: projectsRouter,
analytics: analyticsRouter,
});
export type AppRouter = typeof appRouter;
#Subscriptions WebSocket
tRPC supporte les subscriptions temps réel via WebSocket. Configuration dans init.ts avec un transport WebSocket séparé du transport HTTP — pertinent pour les notifications en temps réel sur les applications B2B Next.js avec tableaux de bord live.
import { observable } from '@trpc/server/observable';
import EventEmitter from 'events';
const ee = new EventEmitter();
export const notificationsRouter = router({
onNewNotification: protectedProcedure
.input(z.object({ userId: z.string() }))
.subscription(({ input }) => {
return observable<Notification>((emit) => {
const onNotification = (data: Notification) => {
if (data.userId === input.userId) {
emit.next(data);
}
};
ee.on('notification', onNotification);
return () => ee.off('notification', onNotification);
});
}),
});
#Middleware et gestion de l'authentification dans tRPC
Les middlewares tRPC sont des fonctions qui s'exécutent avant les procedures. Ils permettent de centraliser l'authentification, le logging, le rate limiting et tout autre comportement transversal.
#Middleware d'authentification
// src/server/trpc/middlewares/auth.ts
import { TRPCError } from '@trpc/server';
import { middleware, procedure as publicProcedure } from '../init';
const isAuthenticated = middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Vous devez être connecté pour accéder à cette ressource',
});
}
return next({
ctx: {
...ctx,
session: ctx.session, // TypeScript sait maintenant que session est non-null
user: ctx.session.user, // User typé et garanti non-null
},
});
});
export const protectedProcedure = publicProcedure.use(isAuthenticated);
#Middleware de rôle (RBAC)
// src/server/trpc/middlewares/roles.ts
import { TRPCError } from '@trpc/server';
import { middleware } from '../init';
type Role = 'admin' | 'manager' | 'viewer';
export const requireRole = (requiredRole: Role) =>
middleware(({ ctx, next }) => {
const userRole = ctx.session?.user?.role as Role | undefined;
const roleHierarchy: Record<Role, number> = {
admin: 3,
manager: 2,
viewer: 1,
};
if (!userRole || roleHierarchy[userRole] < roleHierarchy[requiredRole]) {
throw new TRPCError({
code: 'FORBIDDEN',
message: `Rôle requis : ${requiredRole}. Votre rôle : ${userRole ?? 'non défini'}`,
});
}
return next({ ctx });
});
export const adminProcedure = protectedProcedure.use(requireRole('admin'));
export const managerProcedure = protectedProcedure.use(requireRole('manager'));
#Middleware de logging et performance
// src/server/trpc/middlewares/logger.ts
import { middleware } from '../init';
export const loggerMiddleware = middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const durationMs = Date.now() - start;
if (result.ok) {
console.log(`[tRPC] ${type} ${path} — ${durationMs}ms OK`);
} else {
console.error(`[tRPC] ${type} ${path} — ${durationMs}ms ERROR`, result.error);
}
return result;
});
La composition de middlewares dans tRPC est explicite et statiquement typée — contrairement aux middlewares Express où les types du contexte enrichis ne sont pas propagés au handler suivant. L'intégration avec les patterns de l'App Router Next.js vs Pages Router est directe : un seul middleware pour tous les endpoints tRPC, pas de duplication par route.
→ Vous évaluez vos options ? Utilisez notre estimateur de budget en ligne pour obtenir une fourchette en 2 minutes, ou consultez nos tarifs détaillés.
#Validation avec Zod : types partagés frontend/backend
Zod est le partenaire naturel de tRPC. Les schémas Zod servent à la fois de validation runtime et de source des types TypeScript — une seule définition pour les deux usages.
#Schémas partagés dans un monorepo
Dans l'architecture monorepo Turborepo recommandée pour les projets B2B, les schémas Zod vivent dans un package partagé libs/schemas :
// libs/schemas/src/user.ts
import { z } from 'zod';
export const UserRoleSchema = z.enum(['admin', 'manager', 'viewer']);
export type UserRole = z.infer<typeof UserRoleSchema>;
export const UserSchema = z.object({
id: z.string().cuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: UserRoleSchema,
createdAt: z.date(),
});
export type User = z.infer<typeof UserSchema>;
export const CreateUserInput = z.object({
email: z.string().email('Email invalide'),
name: z.string().min(2, 'Nom trop court').max(100, 'Nom trop long'),
role: UserRoleSchema.default('viewer'),
});
export type CreateUserInput = z.infer<typeof CreateUserInput>;
export const UpdateUserInput = CreateUserInput.partial().extend({
id: z.string().cuid(),
});
export type UpdateUserInput = z.infer<typeof UpdateUserInput>;
#Réutilisation frontend : validation de formulaire
Le même schéma Zod utilisé dans la procedure tRPC peut être réutilisé dans le formulaire React avec react-hook-form :
// src/components/forms/CreateUserForm.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { CreateUserInput } from '@monorepo/schemas/user';
import { api } from '@/lib/trpc/client';
export function CreateUserForm() {
const form = useForm<CreateUserInput>({
resolver: zodResolver(CreateUserInput),
defaultValues: { role: 'viewer' },
});
const createUser = api.users.create.useMutation({
onSuccess: () => {
form.reset();
},
});
const onSubmit = (data: CreateUserInput) => {
createUser.mutate(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* champs du formulaire */}
</form>
);
}
La validation s'applique deux fois : côté client via zodResolver (UX immédiate), côté serveur dans la procedure tRPC (sécurité garantie). Un seul schéma, deux niveaux de protection — c'est précisément l'approche préconisée dans notre guide sur la validation et les Structured Outputs.
#Transformations et coercions
Zod gère les transformations de type qui sont courantes dans les APIs :
const queryInput = z.object({
// Coerce string -> number pour les query params URL
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
// Date depuis string ISO
startDate: z.coerce.date().optional(),
// Trim et lowercase sur les strings
search: z.string().trim().toLowerCase().optional(),
});
#Gestion des erreurs et types d'erreurs customisés
tRPC propose un système d'erreurs structuré qui va bien au-delà des codes HTTP.
#Codes d'erreur tRPC
tRPC mappe ses codes d'erreur vers des statuts HTTP standards :
| Code tRPC | Statut HTTP | Cas d'usage |
|---|---|---|
BAD_REQUEST | 400 | Input invalide non attrapé par Zod |
UNAUTHORIZED | 401 | Non authentifié |
FORBIDDEN | 403 | Authentifié mais non autorisé |
NOT_FOUND | 404 | Ressource introuvable |
CONFLICT | 409 | Conflit (email déjà utilisé) |
PRECONDITION_FAILED | 412 | Précondition métier non remplie |
UNPROCESSABLE_CONTENT | 422 | Données valides syntaxiquement mais invalides métier |
TOO_MANY_REQUESTS | 429 | Rate limiting |
INTERNAL_SERVER_ERROR | 500 | Erreur serveur inattendue |
#Erreurs métier typées
Pour les erreurs métier complexes, le pattern recommandé est de créer des classes d'erreur spécifiques :
// src/server/errors/business-errors.ts
import { TRPCError } from '@trpc/server';
export class InsufficientCreditsError extends TRPCError {
constructor(required: number, available: number) {
super({
code: 'PRECONDITION_FAILED',
message: `Crédits insuffisants. Requis : ${required}, disponibles : ${available}`,
cause: { required, available, type: 'INSUFFICIENT_CREDITS' },
});
}
}
export class ProjectLimitReachedError extends TRPCError {
constructor(limit: number) {
super({
code: 'PRECONDITION_FAILED',
message: `Limite de projets atteinte (${limit} projets maximum sur votre plan)`,
cause: { limit, type: 'PROJECT_LIMIT_REACHED' },
});
}
}
#Gestion côté client avec React Query
// src/components/ProjectCreator.tsx
'use client';
import { api } from '@/lib/trpc/client';
import { TRPCClientError } from '@trpc/client';
import type { AppRouter } from '@/server/trpc/routers/_app';
export function ProjectCreator() {
const createProject = api.projects.create.useMutation({
onError(error) {
// Type-safe : error est typé selon AppRouter
if (error instanceof TRPCClientError) {
const cause = error.data?.cause as { type?: string } | undefined;
if (cause?.type === 'PROJECT_LIMIT_REACHED') {
// Afficher un upsell vers le plan supérieur
showUpgradeModal();
return;
}
// Erreurs Zod structurées
if (error.data?.zodError) {
const fieldErrors = error.data.zodError.fieldErrors;
// Mapper les erreurs vers les champs du formulaire
}
}
},
});
// ...
}
La gestion d'erreurs typée est l'un des arguments les plus solides pour tRPC dans un contexte B2B : les erreurs métier spécifiques sont détectables statiquement côté client, pas seulement en lisant la documentation.
#Testing des routes tRPC : unit tests et intégration
Le testing de tRPC bénéficie de sa nature TypeScript-first : les procedures sont des fonctions pures testables sans mocking HTTP.
#Unit tests avec le caller direct
// src/server/trpc/routers/projects.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createCallerFactory } from '../init';
import { appRouter } from './_app';
import { createMockContext } from '@/tests/helpers/mock-context';
const createCaller = createCallerFactory(appRouter);
describe('projectsRouter', () => {
describe('create', () => {
it('crée un projet pour un utilisateur authentifié', async () => {
const ctx = createMockContext({
session: {
user: { id: 'user-123', email: 'test@example.com', role: 'manager' },
expires: '2027-01-01',
},
});
const caller = createCaller(ctx);
const project = await caller.projects.create({
name: 'Projet de test',
description: 'Description du projet',
});
expect(project.name).toBe('Projet de test');
expect(project.status).toBe('draft');
expect(project.userId).toBe('user-123');
});
it('rejette la création sans authentification', async () => {
const ctx = createMockContext({ session: null });
const caller = createCaller(ctx);
await expect(
caller.projects.create({ name: 'Projet' })
).rejects.toThrow('UNAUTHORIZED');
});
it('valide les inputs avec Zod', async () => {
const ctx = createMockContext({ session: { user: { id: 'u-1' } } });
const caller = createCaller(ctx);
await expect(
caller.projects.create({ name: 'x' }) // trop court (min 2)
).rejects.toThrow();
});
});
});
#Helper de contexte mock
// src/tests/helpers/mock-context.ts
import { vi } from 'vitest';
import type { Context } from '@/server/trpc/init';
export function createMockContext(overrides: Partial<Context> = {}): Context {
return {
session: null,
headers: new Headers(),
db: {
project: {
findMany: vi.fn().mockResolvedValue([]),
findUnique: vi.fn().mockResolvedValue(null),
create: vi.fn().mockImplementation((args) => ({
id: 'proj-mock-1',
...args.data,
createdAt: new Date(),
updatedAt: new Date(),
})),
delete: vi.fn().mockResolvedValue(undefined),
},
},
...overrides,
};
}
#Tests d'intégration avec le Route Handler
Pour les tests d'intégration qui valident le transport HTTP, on peut utiliser les utilitaires de testing de @trpc/server :
// src/tests/integration/trpc-http.test.ts
import { describe, it, expect } from 'vitest';
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import { appRouter } from '@/server/trpc/routers/_app';
import type { AppRouter } from '@/server/trpc/routers/_app';
describe('tRPC HTTP integration', () => {
it('effectue un batch de queries', async () => {
const server = createHTTPServer({
router: appRouter,
createContext: () => createMockContext({
session: { user: { id: 'u-1', email: 'test@test.com', role: 'admin' } },
}),
});
server.listen(0);
const { port } = server.server.address() as { port: number };
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `http://localhost:${port}`,
transformer: superjson,
}),
],
});
const result = await client.projects.list.query({ limit: 5 });
expect(result.items).toBeInstanceOf(Array);
server.server.close();
});
});
Cette approche de testing s'intègre naturellement avec Playwright pour les tests E2E : les tests unitaires couvrent la logique tRPC, les tests E2E couvrent le comportement utilisateur de bout en bout. La qualité code maintenue par les standards Nehos exige les deux niveaux sur les projets B2B.
#Performance et limitations : quand tRPC n'est pas le bon choix
tRPC est un outil excellent dans son domaine. Mais comme tout outil, il a des limites réelles qu'il faut connaître avant d'architecturer un projet.
#Limitations techniques
Ecosystème TypeScript exclusif : tRPC ne fonctionne que dans un environnement TypeScript. Si vous avez une app mobile en Swift, une intégration en Python, ou un partenaire qui consomme votre API en Go, tRPC ne peut pas leur exposer une interface typée. Pour ces cas, REST + OpenAPI ou GraphQL restent incontournables.
Pas de spec OpenAPI native : contrairement à REST, tRPC ne génère pas de documentation d'API lisible par des outils standards. Il existe des adapters comme trpc-openapi qui permettent d'exposer des endpoints REST depuis des procedures tRPC — utile pour les cas hybrides. Mais le workflow de documentation reste plus complexe qu'avec une API REST native.
Couplage fort frontend/backend : le contrat implicite est une feature pour les équipes internes, mais un risque pour les projets où le backend peut être réutilisé par plusieurs frontends non-TypeScript. Si vous anticipez d'exposer votre backend à des consommateurs externes dans 2 ans, concevez votre API REST dès maintenant.
Pas de cache HTTP natif : contrairement aux requêtes GET REST qui bénéficient du cache HTTP (CDN, navigateur), les queries tRPC via httpBatchLink utilisent des POST par défaut — non cacheables par les CDN. Pour les données fortement cacheable, il faut configurer httpLink sur les queries spécifiques ou utiliser le mécanisme de cache React / Next.js au niveau application.
#Limitations organisationnelles
Monorepo ou repos couplés requis : tRPC nécessite que le type AppRouter soit accessible côté client. En pratique, cela signifie un monorepo ou une dépendance de package entre les repos. Si vos équipes frontend et backend ont des processus de release totalement indépendants, le couplage tRPC peut devenir une friction organisationnelle.
Courbe d'apprentissage pour les développeurs junior : la magie du type-inference tRPC peut être déroutante pour les développeurs moins familiers avec TypeScript avancé. Les erreurs de type au niveau des middlewares ou des context-merging peuvent être cryptiques. Un développeur senior doit généralement poser les fondations.
#Quand choisir autre chose
Préférez REST + OpenAPI si : API publique ou consommée par des tiers, équipes frontend/backend séparées avec releases indépendantes, besoin de versionning d'API explicite, stack non-TypeScript.
Préférez GraphQL si : données fortement relationnelles, multiples clients avec besoins de sélection différents, fédération de plusieurs services, partenaires qui bénéficient de la flexibilité des queries.
tRPC est le bon choix si : application full-stack TypeScript dans un monorepo, API consommée uniquement par votre propre frontend, équipe qui valorise la productivité et la sécurité de type au détriment de l'interopérabilité externe, projet où la vélocité de développement est un critère clé.
#Impact sur les performances Next.js
Avec App Router, le pattern optimal est d'utiliser le caller direct dans les Server Components (zéro overhead HTTP) et le client React Query uniquement pour les mutations et les queries qui nécessitent une réactivité côté client. Cette stratégie duale maximise les performances : les données initiales sont chargées côté serveur sans latence réseau, les interactions utilisateur passent par le client HTTP standard.
Pour les projets qui doivent aussi exposer une API publique, l'approche hybride REST + tRPC est viable : les endpoints publics sont des Route Handlers Next.js classiques, les endpoints internes utilisent tRPC. Les patterns d'API-first design pour B2B restent applicables — tRPC et REST ne sont pas mutuellement exclusifs dans une même application.
#Cas d'usage Nehos en production
Sur les projets clients Nehos en refonte B2B, tRPC est systématiquement recommandé pour les portails clients et back-offices Next.js où l'équipe de développement est unique et le frontend est exclusivement Next.js. Sur les projets avec des contraintes d'interopérabilité externe — notamment les intégrations avec des CMS headless comme Payload CMS qui exposent leur propre API REST — l'approche hybride est préférée.
La maturité de tRPC en 2026 permet de l'adopter sur des projets de production sans réserve technique — à condition de choisir le bon périmètre d'application.