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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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âches
  • task.create — création d'une tâche avec validation Zod
  • task.update — mise à jour partielle protégée par middleware
  • task.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 :

  1. 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.
  2. 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.
  3. 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-demo

Ré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 zod

Le 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 contract

Remarquez 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 router

L'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 = handle

Maintenant /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 dev

Ensuite, 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 onMutate de 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Créer et déployer une API serverless avec Cloudflare Workers, Hono et D1.

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.

30 min read·