Construire une API Production-Ready avec tRPC, Prisma et Next.js

Construire une API Production-Ready avec tRPC, Prisma et Next.js
Construire des APIs modernes nécessite d'équilibrer l'expérience développeur, la sécurité des types et les performances. Les APIs REST traditionnelles manquent de sécurité des types end-to-end, tandis que GraphQL ajoute une complexité significative. tRPC offre une solution élégante : sécurité complète des types TypeScript sans génération de code, combinée avec Prisma pour la gestion de base de données et Next.js 15 pour le rendu serveur et client.
À la fin de ce tutoriel, vous aurez construit une API complète de gestion de tâches avec authentification, opérations de base de données et un client React—le tout avec une sécurité complète des types de la base de données à l'interface utilisateur.
Pourquoi tRPC + Prisma ?
tRPC (TypeScript Remote Procedure Call) élimine le problème du contrat API. Lorsque vous modifiez votre backend, vos types frontend se mettent à jour automatiquement. Pas de documentation API manuelle, pas de schémas OpenAPI, pas d'étapes de génération de code.
Prisma fournit un ORM type-safe avec migrations, introspection et une excellente DX. Combiné avec tRPC, vous obtenez la sécurité des types du schéma de base de données aux composants React.
Avantages réels :
- Détecter les erreurs à la compilation, pas à l'exécution
- Refactoriser en toute confiance (renommez un champ, TypeScript trouve toutes les utilisations)
- Autocomplétion partout
- Moins de code boilerplate comparé à REST ou GraphQL
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 18+ (vérifier : `node -v`)
- npm, yarn ou pnpm (nous utiliserons pnpm)
- PostgreSQL installé localement ou une base de données cloud
- Connaissances de base en TypeScript et React
- Familiarité avec les fondamentaux de Next.js
Installer PostgreSQL :
# macOS
brew install postgresql@15
brew services start postgresql@15
# Ubuntu/Debian
sudo apt update && sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresqlÉtape 1 : Configuration du Projet
Créer un nouveau projet Next.js :
pnpm create next-app@latest trpc-task-api --typescript --tailwind --app --eslint
cd trpc-task-apiInstaller les dépendances :
pnpm add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query@^5
pnpm add @prisma/client zod superjson
pnpm add -D prismaInitialiser Prisma :
pnpx prisma initÉtape 2 : Schéma de Base de Données
Modifier `prisma/schema.prisma` :
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
tasks Task[]
}
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
enum TaskStatus {
TODO
IN_PROGRESS
DONE
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}Mettre à jour `.env` :
DATABASE_URL="postgresql://postgres:password@localhost:5432/trpc_tasks"
Exécuter la migration :
pnpx prisma migrate dev --name initÉtape 3 : Client Prisma
Créer `lib/prisma.ts` :
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prismaPourquoi ce pattern ? Next.js hot-reload crée plusieurs instances Prisma en développement. Ce pattern singleton évite l'erreur "trop de connexions".
Étape 4 : Configuration tRPC
Créer `lib/trpc/trpc.ts` :
import { initTRPC, TRPCError } from '@trpc/server'
import { prisma } from '@/lib/prisma'
import superjson from 'superjson'
export const createTRPCContext = async (opts: { headers: Headers }) => {
return {
prisma,
userId: opts.headers.get('x-user-id') || undefined,
}
}
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
})
export const router = t.router
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(async (opts) => {
if (!opts.ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return opts.next({ ctx: { ...opts.ctx, userId: opts.ctx.userId } })
})Concepts clés :
- Context – Données disponibles pour toutes les procédures (base de données, session utilisateur, etc.)
- Transformer – `superjson` permet les objets Date et autres types non-JSON
- Procedures – Comme des fonctions exposées via l'API
- Middleware – `protectedProcedure` vérifie l'authentification
Étape 5 : Router de Tâches
Créer `lib/trpc/routers/task.ts` :
import { z } from 'zod'
import { router, protectedProcedure } from '../trpc'
import { TaskStatus, Priority } from '@prisma/client'
import { TRPCError } from '@trpc/server'
export const taskRouter = router({
list: protectedProcedure
.input(z.object({
status: z.nativeEnum(TaskStatus).optional(),
limit: z.number().min(1).max(100).default(50)
}))
.query(async ({ ctx, input }) => {
return await ctx.prisma.task.findMany({
where: {
userId: ctx.userId,
...(input.status && { status: input.status })
},
orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }],
take: input.limit,
})
}),
getById: protectedProcedure
.input(z.object({ id: z.string().cuid() }))
.query(async ({ ctx, input }) => {
const task = await ctx.prisma.task.findUnique({ where: { id: input.id } })
if (!task || task.userId !== ctx.userId) {
throw new TRPCError({ code: 'NOT_FOUND' })
}
return task
}),
create: protectedProcedure
.input(z.object({
title: z.string().min(1).max(200),
description: z.string().max(2000).optional(),
priority: z.nativeEnum(Priority).default('MEDIUM'),
}))
.mutation(async ({ ctx, input }) => {
return await ctx.prisma.task.create({
data: { ...input, userId: ctx.userId },
})
}),
update: protectedProcedure
.input(z.object({
id: z.string().cuid(),
title: z.string().min(1).max(200).optional(),
description: z.string().max(2000).optional().nullable(),
status: z.nativeEnum(TaskStatus).optional(),
priority: z.nativeEnum(Priority).optional(),
}))
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input
const existing = await ctx.prisma.task.findUnique({ where: { id } })
if (!existing || existing.userId !== ctx.userId) {
throw new TRPCError({ code: 'NOT_FOUND' })
}
return await ctx.prisma.task.update({ where: { id }, data })
}),
delete: protectedProcedure
.input(z.object({ id: z.string().cuid() }))
.mutation(async ({ ctx, input }) => {
const existing = await ctx.prisma.task.findUnique({ where: { id: input.id } })
if (!existing || existing.userId !== ctx.userId) {
throw new TRPCError({ code: 'NOT_FOUND' })
}
await ctx.prisma.task.delete({ where: { id: input.id } })
return { success: true }
}),
stats: protectedProcedure.query(async ({ ctx }) => {
const [total, todo, inProgress, done] = await Promise.all([
ctx.prisma.task.count({ where: { userId: ctx.userId } }),
ctx.prisma.task.count({ where: { userId: ctx.userId, status: 'TODO' } }),
ctx.prisma.task.count({ where: { userId: ctx.userId, status: 'IN_PROGRESS' } }),
ctx.prisma.task.count({ where: { userId: ctx.userId, status: 'DONE' } }),
])
return { total, todo, inProgress, done }
}),
})Structure du router :
- Validation des entrées – Les schémas Zod empêchent les données invalides
- Autorisation – Vérification de la propriété des tâches avant modifications
- Inférence des types – TypeScript connaît les types exacts entrée/sortie
Étape 6 : Router Principal
Créer `lib/trpc/root.ts` :
import { router } from './trpc'
import { taskRouter } from './routers/task'
export const appRouter = router({
task: taskRouter,
})
export type AppRouter = typeof appRouterÉtape 7 : Route API
Créer `app/api/trpc/[trpc]/route.ts` :
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/lib/trpc/root'
import { createTRPCContext } from '@/lib/trpc/trpc'
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: createTRPCContext,
})
export { handler as GET, handler as POST }Étape 8 : Configuration Client
Créer `lib/trpc/client.ts` :
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './root'
export const trpc = createTRPCReact<AppRouter>()Créer `app/providers.tsx` :
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import { trpc } from '@/lib/trpc/client'
import { useState } from 'react'
import superjson from 'superjson'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient())
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: \`\${getBaseUrl()}/api/trpc\`,
transformer: superjson,
headers() {
return { 'x-user-id': 'demo-user-123' }
},
}),
],
})
)
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
)
}
function getBaseUrl() {
if (typeof window !== 'undefined') return ''
if (process.env.VERCEL_URL) return \`https://\${process.env.VERCEL_URL}\`
return \`http://localhost:\${process.env.PORT ?? 3000}\`
}Mettre à jour `app/layout.tsx` :
import { Providers } from './providers'
import './globals.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fr">
<body><Providers>{children}</Providers></body>
</html>
)
}Étape 9 : Composant d'Interface
Créer `app/tasks/page.tsx` :
'use client'
import { trpc } from '@/lib/trpc/client'
import { useState } from 'react'
export default function TasksPage() {
const [title, setTitle] = useState('')
const utils = trpc.useUtils()
const { data: tasks, isLoading } = trpc.task.list.useQuery({ limit: 50 })
const { data: stats } = trpc.task.stats.useQuery()
const createTask = trpc.task.create.useMutation({
onSuccess: () => {
utils.task.list.invalidate()
utils.task.stats.invalidate()
setTitle('')
},
})
const updateTask = trpc.task.update.useMutation({
onSuccess: () => {
utils.task.list.invalidate()
utils.task.stats.invalidate()
},
})
const deleteTask = trpc.task.delete.useMutation({
onSuccess: () => {
utils.task.list.invalidate()
utils.task.stats.invalidate()
},
})
if (isLoading) return <div className="p-8">Chargement...</div>
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Gestionnaire de Tâches</h1>
{stats && (
<div className="grid grid-cols-4 gap-4 mb-8">
<div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.total}</div><div className="text-sm text-gray-600">Total</div></div>
<div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.todo}</div><div className="text-sm text-gray-600">À Faire</div></div>
<div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.inProgress}</div><div className="text-sm text-gray-600">En Cours</div></div>
<div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.done}</div><div className="text-sm text-gray-600">Terminé</div></div>
</div>
)}
<form onSubmit={(e) => { e.preventDefault(); createTask.mutate({ title }) }} className="mb-8">
<div className="flex gap-2">
<input type="text" value={title} onChange={(e) => setTitle(e.target.value)}
placeholder="Nouvelle tâche..." className="flex-1 px-4 py-2 border rounded" />
<button type="submit" disabled={createTask.isPending}
className="px-6 py-2 bg-blue-600 text-white rounded">Ajouter</button>
</div>
</form>
<div className="space-y-2">
{tasks?.map((task) => (
<div key={task.id} className="flex items-center gap-4 p-4 border rounded">
<input type="checkbox" checked={task.status === 'DONE'}
onChange={() => updateTask.mutate({ id: task.id, status: task.status === 'DONE' ? 'TODO' : 'DONE' })} />
<div className="flex-1">
<h3 className={\`font-medium \${task.status === 'DONE' ? 'line-through' : ''}\`}>{task.title}</h3>
</div>
<span className="px-2 py-1 text-xs rounded bg-gray-100">{task.priority}</span>
<button onClick={() => deleteTask.mutate({ id: task.id })} className="text-red-600">Supprimer</button>
</div>
))}
</div>
</div>
)
}Étape 10 : Exécuter l'Application
pnpm devVisiter http://localhost:3000/tasks
Meilleures Pratiques Production
1. Authentification
Remplacer l'en-tête démo par NextAuth.js ou Clerk :
import { getServerSession } from 'next-auth'
export const createTRPCContext = async () => {
const session = await getServerSession()
return { prisma, userId: session?.user?.id }
}2. Limitation de Débit
Utiliser Upstash ou similaire pour limiter les requêtes abusives.
3. Surveillance des Erreurs
Ajouter Sentry ou un outil similaire pour tracer les erreurs en production.
4. Pooling de Connexions
Pour serverless, utiliser Prisma Accelerate ou des poolers comme PgBouncer.
5. Mise en Cache
Implémenter le cache Redis pour les données fréquemment consultées.
Déploiement
Vercel
pnpx vercelDéfinir `DATABASE_URL` dans les variables d'environnement.
Docker
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
COPY . .
RUN pnpx prisma generate
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]Résumé
Vous avez construit une API production-ready avec :
✅ Sécurité des types end-to-end de la base de données à l'interface ✅ Validation des entrées avec Zod ✅ Authentification et autorisation ✅ Migrations de base de données avec Prisma ✅ Cache et mises à jour optimistes avec React Query ✅ Batching des requêtes pour les performances
Points Clés
- tRPC élimine les contrats API – les types se synchronisent automatiquement
- Les types Prisma affluent vers tRPC – le schéma de base de données pilote les types API
- Les middlewares activent les patterns réutilisables – auth, logging, validation
- React Query alimente le client – cache, retries, mutations
- TypeScript détecte les erreurs tôt – sécurité à la compilation
Prochaines Étapes
- Ajouter des abonnements WebSocket pour les mises à jour temps réel
- Implémenter le téléchargement de fichiers
- Ajouter un endpoint OpenAPI pour les intégrations tierces
- Construire une app mobile avec React Native + tRPC
- Implémenter des patterns CQRS avec bases séparées
Besoin d'Aide pour Construire des APIs Production ?
Noqta se spécialise dans la construction d'APIs robustes et type-safe et d'applications full-stack :
- Intégration API – Connecter les systèmes avec des APIs fiables
- Développement Web – Applications Next.js, React, Node.js
- Assurance Qualité – Tests, audits, optimisation
Réserver une consultation gratuite : Contacter Noqta
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

AI SDK 4.0 : Nouvelles Fonctionnalites et Cas d'Utilisation
Decouvrez les nouvelles fonctionnalites et cas d'utilisation d'AI SDK 4.0, incluant le support PDF, l'utilisation de l'ordinateur et plus encore.

Créer un Web Scraper Intelligent avec Playwright et l'API Claude en TypeScript
Apprenez à construire un scraper web intelligent qui utilise Playwright pour l'automatisation du navigateur et l'IA Claude pour extraire, nettoyer et structurer les données de n'importe quel site — sans sélecteurs CSS fragiles.

Construire une Application d'IA Conversationnelle avec Next.js
Apprenez a construire une application web qui permet des conversations vocales en temps reel avec des agents IA en utilisant Next.js et ElevenLabs.