Construire des API type-safe avec ORPC et Next.js 15 en 2026

Si vous aimez tRPC mais regrettez qu'il ne fournisse pas nativement OpenAPI, la compatibilité REST et un workflow contract-first plus propre, ORPC est la bibliothèque qui comble enfin ces manques. En 2026, elle est discrètement devenue le choix de référence pour les équipes qui veulent la sécurité de typage de bout en bout sans se verrouiller à un seul transport ou framework.
Ce tutoriel vous guide dans la construction d'une API ORPC prête pour la production à l'intérieur d'un projet Next.js 15 avec App Router. À la fin, vous aurez des procédures type-safe, un client React propulsé par TanStack Query, un middleware d'authentification, la gestion des erreurs et une documentation OpenAPI générée automatiquement — le tout en moins de 30 minutes de travail pratique.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (ORPC utilise les API modernes
ReadableStream) - Bun ou pnpm (les exemples utilisent pnpm, mais l'un ou l'autre fonctionne)
- Une familiarité avec TypeScript et React
- Des connaissances de base de Next.js App Router
- Un éditeur de code avec support TypeScript (VS Code recommandé)
Ce que vous allez construire
Une petite API de gestionnaire de tâches exposant :
task.list— liste paginée des tâchestask.create— création d'une tâche avec validation Zodtask.update— mise à jour partielle protégée par middlewaretask.delete— suppression avec vérification d'autorisation
Les mêmes procédures seront consommées de trois manières différentes : depuis un composant React, depuis un simple appel fetch frappant la surface REST, et depuis une visionneuse OpenAPI générée automatiquement par ORPC.
Pourquoi ORPC au lieu de tRPC ?
ORPC (prononcé "oh-arpec") conserve tout ce que les développeurs aiment dans tRPC — types inférés, chaînes de middlewares, erreurs typées — et ajoute trois choses qui ont suffi à justifier une nouvelle bibliothèque :
- Schémas contract-first. Vous pouvez définir un contrat de procédure dans un paquet et l'implémenter dans un autre, ce qui est parfait pour les monorepos où l'application mobile n'utilise pas encore Next.js.
- Double transport. Chaque procédure est appelable via RPC et REST. Les clients web utilisent le transport RPC binaire pour la vitesse ; les tiers frappent le point de terminaison REST et lisent la documentation OpenAPI.
- Agnosticisme de framework. Le même routeur tourne sur Next.js, Hono, Elysia, Bun, Cloudflare Workers ou un serveur Node ordinaire. Migrer plus tard devient un changement d'une ligne.
Étape 1 : Créer le projet Next.js
Partez d'un nouveau projet Next.js 15 avec App Router et TypeScript :
pnpm create next-app@latest orpc-demo --typescript --app --tailwind --src-dir
cd orpc-demoRépondez Non à ESLint si vous prévoyez d'utiliser Biome plus tard, et Non à Turbopack pour garder la parité entre le développement et la production.
Étape 2 : Installer ORPC et les paquets associés
ORPC est divisé en paquets ciblés afin que vous ne payiez que ce que vous importez.
pnpm add @orpc/server @orpc/client @orpc/contract @orpc/openapi
pnpm add @orpc/react-query @tanstack/react-query
pnpm add zodLe paquet @orpc/contract est optionnel mais recommandé — il permet de définir les schémas une seule fois et de les réutiliser côté serveur et côté client.
Étape 3 : Définir votre contrat
Créez un nouveau fichier qui décrit la forme de chaque procédure. Considérez-le comme votre alternative native TypeScript à un fichier YAML OpenAPI.
// src/server/contract.ts
import { oc } from '@orpc/contract'
import { z } from 'zod'
const TaskSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(120),
done: z.boolean(),
createdAt: z.date(),
})
export const contract = oc.router({
task: {
list: oc
.route({ method: 'GET', path: '/tasks' })
.input(z.object({ cursor: z.string().optional(), limit: z.number().int().min(1).max(100).default(20) }))
.output(z.object({ items: z.array(TaskSchema), nextCursor: z.string().nullable() })),
create: oc
.route({ method: 'POST', path: '/tasks' })
.input(z.object({ title: z.string().min(1).max(120) }))
.output(TaskSchema),
update: oc
.route({ method: 'PATCH', path: '/tasks/{id}' })
.input(z.object({ id: z.string().uuid(), title: z.string().optional(), done: z.boolean().optional() }))
.output(TaskSchema),
delete: oc
.route({ method: 'DELETE', path: '/tasks/{id}' })
.input(z.object({ id: z.string().uuid() }))
.output(z.object({ success: z.literal(true) })),
},
})
export type Contract = typeof contractRemarquez que chaque procédure reçoit un chemin REST. ORPC servira à la fois le format binaire RPC et le chemin REST à partir du même routeur — sans duplication.
Étape 4 : Implémenter le routeur
Connectez maintenant la logique métier au contrat. Pour ce tutoriel, nous utiliserons un magasin en mémoire, mais remplacez-le par Prisma, Drizzle ou Supabase dans votre vrai projet.
// src/server/router.ts
import { implement } from '@orpc/server'
import { randomUUID } from 'node:crypto'
import { contract } from './contract'
type Task = {
id: string
title: string
done: boolean
createdAt: Date
}
const store = new Map<string, Task>()
const os = implement(contract)
const taskListHandler = os.task.list.handler(async ({ input }) => {
const items = Array.from(store.values()).slice(0, input.limit)
return { items, nextCursor: null }
})
const taskCreateHandler = os.task.create.handler(async ({ input }) => {
const task: Task = {
id: randomUUID(),
title: input.title,
done: false,
createdAt: new Date(),
}
store.set(task.id, task)
return task
})
const taskUpdateHandler = os.task.update.handler(async ({ input, errors }) => {
const existing = store.get(input.id)
if (!existing) throw errors.NOT_FOUND({ message: 'Task not found' })
const updated = { ...existing, ...input }
store.set(updated.id, updated)
return updated
})
const taskDeleteHandler = os.task.delete.handler(async ({ input, errors }) => {
if (!store.has(input.id)) throw errors.NOT_FOUND({ message: 'Task not found' })
store.delete(input.id)
return { success: true as const }
})
export const router = os.router({
task: {
list: taskListHandler,
create: taskCreateHandler,
update: taskUpdateHandler,
delete: taskDeleteHandler,
},
})
export type Router = typeof routerL'appel à implement(contract) garantit à la compilation que chaque gestionnaire correspond au contrat — renommez un champ et TypeScript signalera instantanément le gestionnaire cassé.
Étape 5 : Monter ORPC sur un gestionnaire de route Next.js
App Router rend cela trivial. Créez une route catch-all qui transmet chaque requête au gestionnaire ORPC.
// src/app/api/[[...rest]]/route.ts
import { RPCHandler } from '@orpc/server/fetch'
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { router } from '@/server/router'
const rpcHandler = new RPCHandler(router)
const openapiHandler = new OpenAPIHandler(router)
async function handle(request: Request) {
const url = new URL(request.url)
if (url.pathname.startsWith('/api/rpc')) {
const { response } = await rpcHandler.handle(request, { prefix: '/api/rpc' })
if (response) return response
}
const { response } = await openapiHandler.handle(request, { prefix: '/api' })
if (response) return response
return new Response('Not found', { status: 404 })
}
export const GET = handle
export const POST = handle
export const PATCH = handle
export const DELETE = handleMaintenant /api/rpc/task.list sert les appels RPC binaires et /api/tasks sert exactement les mêmes données via REST. Un routeur, deux transports.
Étape 6 : Construire le client
Créez un helper côté client qui réutilise le même type Router pour que chaque appel soit auto-complété et type-checké.
// src/lib/orpc-client.ts
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import type { Router } from '@/server/router'
const link = new RPCLink({ url: '/api/rpc' })
export const orpc = createORPCClient<Router>(link)Grâce au type Router exporté, orpc.task.create({ title: 'Ship ORPC demo' }) est entièrement typé — les entrées, sorties et erreurs possibles sont toutes inférées.
Étape 7 : Connecter TanStack Query
ORPC fournit des liaisons TanStack Query de première classe. Installez le provider à la racine de votre arbre App Router.
// src/app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState, type ReactNode } from 'react'
export function Providers(props: { children: ReactNode }) {
const [client] = useState(() => new QueryClient())
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
}Enveloppez le layout racine :
// src/app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fr">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}Construisez maintenant un composant de liste de tâches qui lit et mute les tâches :
// src/app/page.tsx
'use client'
import { createORPCReactQueryUtils } from '@orpc/react-query'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { orpc } from '@/lib/orpc-client'
const $orpc = createORPCReactQueryUtils(orpc)
export default function HomePage() {
const qc = useQueryClient()
const [title, setTitle] = useState('')
const tasks = useQuery($orpc.task.list.queryOptions({ input: { limit: 20 } }))
const createTask = useMutation({
...$orpc.task.create.mutationOptions(),
onSuccess: () => qc.invalidateQueries({ queryKey: $orpc.task.list.key() }),
})
return (
<main className="mx-auto max-w-xl p-8">
<h1 className="text-2xl font-bold">Tâches</h1>
<form
onSubmit={(e) => {
e.preventDefault()
createTask.mutate({ title })
setTitle('')
}}
className="mt-4 flex gap-2"
>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
className="flex-1 rounded border px-3 py-2"
placeholder="Que faut-il faire ?"
/>
<button className="rounded bg-black px-4 py-2 text-white">Ajouter</button>
</form>
<ul className="mt-6 space-y-2">
{tasks.data?.items.map((task) => (
<li key={task.id} className="rounded border p-3">
{task.title}
</li>
))}
</ul>
</main>
)
}Si vous renommez title en name dans le contrat, TypeScript mettra en évidence l'appel de mutation cassé avant même que vous sauvegardiez le fichier.
Étape 8 : Ajouter un middleware d'authentification
La protection des routes est l'endroit où ORPC semble particulièrement soigné. Les middlewares se composent comme les middlewares Express mais gardent le type de retour entièrement inféré.
// src/server/middleware.ts
import { os } from './router'
import { ORPCError } from '@orpc/server'
import { cookies } from 'next/headers'
export const authed = os.middleware(async ({ context, next }) => {
const session = (await cookies()).get('session')?.value
if (!session) throw new ORPCError('UNAUTHORIZED', { message: 'Sign in to continue' })
const user = { id: session, role: 'member' as const }
return next({ context: { ...context, user } })
})Appliquez-le uniquement aux procédures mutantes :
const taskCreateHandler = os.task.create
.use(authed)
.handler(async ({ input, context }) => {
const task: Task = {
id: randomUUID(),
title: `${input.title} (by ${context.user.id})`,
done: false,
createdAt: new Date(),
}
store.set(task.id, task)
return task
})Les appelants obtiennent désormais une erreur UNAUTHORIZED typée qu'ils peuvent affiner avec if (error.code === 'UNAUTHORIZED').
Étape 9 : Générer automatiquement la documentation OpenAPI
Le paquet @orpc/openapi peut émettre une spécification à partir du même contrat, ce qui signifie que la documentation ne peut jamais diverger de l'implémentation.
// src/app/api/openapi/route.ts
import { OpenAPIGenerator } from '@orpc/openapi'
import { ZodToJsonSchemaConverter } from '@orpc/zod'
import { contract } from '@/server/contract'
const generator = new OpenAPIGenerator({
schemaConverters: [new ZodToJsonSchemaConverter()],
})
export async function GET() {
const spec = await generator.generate(contract, {
info: { title: 'Tasks API', version: '1.0.0' },
servers: [{ url: '/api' }],
})
return Response.json(spec)
}Associez-le à Scalar ou Swagger UI pour obtenir une belle page de documentation sur /api/openapi.
Étape 10 : Gestion des erreurs côté client
Parce que les erreurs font partie du contrat, les clients les gèrent sans deviner :
import { isDefinedError } from '@orpc/client'
try {
await orpc.task.update({ id: 'bad-id', title: 'Nope' })
} catch (error) {
if (isDefinedError(error) && error.code === 'NOT_FOUND') {
console.warn('Task disappeared — refresh the list')
} else {
throw error
}
}Fini de parser des objets Error génériques ou de prier pour que le code de statut corresponde à ce que le backend a renvoyé vendredi dernier.
Tester votre implémentation
Lancez le serveur de développement et testez de bout en bout :
pnpm devEnsuite, exercez les deux transports :
curl http://localhost:3000/api/tasks
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Finish ORPC tutorial"}'Ouvrez http://localhost:3000 dans votre navigateur et ajoutez une tâche depuis le formulaire. Le cache React Query devrait s'invalider automatiquement et la nouvelle tâche devrait apparaître en un clin d'œil.
Dépannage
"Cannot find module '@orpc/server/fetch'" — assurez-vous d'être sur Node 20 ou plus récent. Node 18 ne fournit pas le ReadableStream global qu'ORPC utilise.
Les types sont any côté client — vérifiez que tsconfig.json contient "moduleResolution": "Bundler" et que le type Router est exporté avec export type, pas avec un export nu.
Erreurs CORS en production — ORPC ne définit pas les en-têtes CORS. Utilisez le middleware.ts de Next.js ou un gestionnaire par route pour les ajouter lorsque votre point de terminaison RPC vit sur une origine différente de celle du client.
OpenAPI ne se met pas à jour après un changement de schéma — la spec est générée à la requête, donc un rechargement forcé suffit. Si vous mettez la réponse en cache, n'oubliez pas d'invalider le cache.
Étapes suivantes
- Remplacez le magasin en mémoire par Prisma avec le tutoriel Prisma pour Next.js.
- Ajoutez des mises à jour optimistes via le callback
onMutatede TanStack Query. - Déployez sur Cloudflare Workers — le même routeur tourne sans modification grâce à l'adaptateur Fetch.
- Générez un SDK typé pour votre application mobile en réexportant le contrat depuis un paquet partagé.
- Ajoutez une couche de rate limiting avec Arcjet ou Upstash.
Conclusion
ORPC dans Next.js 15 touche un point d'équilibre que tRPC, REST et GraphQL manquent chacun individuellement. Vous obtenez la sécurité de typage pilotée par contrat à travers le réseau, une surface OpenAPI de première classe pour les consommateurs externes, et un runtime qui vous suit de Vercel à Cloudflare à Bun sans réécritures.
La vraie magie est la façon dont un seul fichier — votre contrat — devient le schéma d'entrée, le type de sortie, la signature du middleware, le document OpenAPI et le hook React Query. Cette source unique de vérité est ce qui rend les équipes plus rapides à mesure que la base de code grandit, et c'est la raison pour laquelle ORPC est devenu notre recommandation par défaut pour les nouvelles API Next.js en 2026.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire une API GraphQL typesafe avec Next.js App Router, Yoga et Pothos
Apprenez à construire une API GraphQL entièrement typesafe avec Next.js 15 App Router, GraphQL Yoga et le constructeur de schémas Pothos. Ce tutoriel pratique couvre la conception de schémas, les requêtes, les mutations, le middleware d'authentification et un client React avec urql.

Medusa.js 2.0 — Construire une boutique e-commerce Headless avec Next.js (2026)
Apprenez à construire une boutique e-commerce complète avec Medusa.js 2.0 et Next.js. Du catalogue produits au paiement, ce tutoriel couvre tout en TypeScript.

Créer des emails transactionnels avec Resend et React Email dans Next.js
Apprenez à créer des emails transactionnels élégants et typés avec React Email et Resend dans une application Next.js. Ce tutoriel couvre la conception de templates, le workflow de prévisualisation, l'envoi via des routes API et le déploiement en production.