Créer des APIs Type-Safe de bout en bout avec tRPC et Next.js App Router

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

La sécurité de type de bout en bout sans génération de code. tRPC vous permet d'appeler des fonctions serveur depuis le client avec autocomplétion TypeScript complète, validation et gestion d'erreurs — pas de schémas REST, pas de résolveurs GraphQL, pas de spécifications OpenAPI. Dans ce tutoriel, vous allez construire un gestionnaire de tâches complet avec Next.js 15 App Router et tRPC.

Ce que vous allez apprendre

À la fin de ce tutoriel, vous saurez :

  • Configurer tRPC v11 avec Next.js 15 App Router en utilisant l'adaptateur fetch
  • Définir des requêtes, mutations et abonnements avec validation Zod
  • Créer des middlewares pour l'authentification et la journalisation
  • Intégrer TanStack React Query v5 pour la récupération de données côté client
  • Utiliser des appelants côté serveur dans les Server Components
  • Gérer les erreurs avec le système d'erreurs intégré de tRPC
  • Construire un gestionnaire de tâches fonctionnel avec des opérations CRUD complètes

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node --version)
  • De l'expérience en TypeScript (types, génériques, inférence)
  • Une familiarité avec Next.js App Router (Server Components, gestionnaires de routes)
  • Les bases de React Query (optionnel, nous couvrirons ce dont vous avez besoin)
  • Un éditeur de code — VS Code ou Cursor recommandé

Pourquoi tRPC ?

Si votre frontend et votre backend sont tous deux en TypeScript, vous partagez déjà un système de types. Alors pourquoi écrire des endpoints REST avec des types requête/réponse séparés, ou maintenir un schéma GraphQL ? tRPC élimine entièrement cette duplication.

FonctionnalitéRESTGraphQLtRPC
Sécurité de typeManuelleGénération de codeAutomatique
Définition de schémaOpenAPISDLAucune nécessaire
Courbe d'apprentissageFaibleMoyenneFaible
Taille du bundleVariableLourdMinimal
Idéal pourAPIs publiquesGraphes complexesApps TS vers TS

tRPC excelle quand votre client et serveur partagent le même codebase TypeScript — ce qui est exactement ce que Next.js vous offre.


Étape 1 : Créer un projet Next.js

Commencez par échafauder un nouveau projet Next.js 15 :

npx create-next-app@latest trpc-task-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd trpc-task-manager

Étape 2 : Installer tRPC et les dépendances

Installez les packages tRPC ainsi que TanStack React Query et Zod :

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod

Voici ce que fait chaque package :

  • @trpc/server — définit votre routeur API, procédures et middleware
  • @trpc/client — client TypeScript vanilla pour appeler votre API
  • @trpc/react-query — hooks React encapsulant TanStack React Query
  • @tanstack/react-query — gestion puissante de l'état asynchrone pour React
  • zod — validation de schéma à l'exécution et inférence de types TypeScript

Étape 3 : Initialiser le backend tRPC

Créez le fichier d'initialisation tRPC. C'est ici que vous définissez votre contexte et vos constructeurs de procédures.

Créez src/trpc/init.ts :

import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
 
// Define the context available to all procedures
export type Context = {
  userId: string | null;
};
 
// Create context for each request
export const createTRPCContext = async (): Promise<Context> => {
  // In a real app, extract user from session/JWT here
  return {
    userId: null,
  };
};
 
// Initialize tRPC — this should only be done once
const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});
 
// Export reusable helpers
export const router = t.router;
export const publicProcedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;
 
// Middleware: require authentication
const enforceAuth = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "You must be logged in to perform this action",
    });
  }
  return next({
    ctx: {
      userId: ctx.userId,
    },
  });
});
 
export const protectedProcedure = t.procedure.use(enforceAuth);

Ce fichier configure trois éléments importants :

  1. Le contexte — les données disponibles pour chaque procédure (comme l'utilisateur courant)
  2. Les procédures publiques — accessibles sans authentification
  3. Les procédures protégées — nécessitent un utilisateur connecté

Étape 4 : Définir le routeur des tâches

Maintenant, créez la logique API proprement dite. Créez src/trpc/routers/task.ts :

import { z } from "zod";
import { router, publicProcedure, protectedProcedure } from "../init";
import { TRPCError } from "@trpc/server";
 
// In-memory store (replace with a real database in production)
interface Task {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
  updatedAt: Date;
}
 
const tasks: Task[] = [
  {
    id: "1",
    title: "Learn tRPC",
    description: "Build a type-safe API with tRPC and Next.js",
    completed: false,
    createdAt: new Date(),
    updatedAt: new Date(),
  },
];
 
// Input validation schemas
const createTaskSchema = z.object({
  title: z.string().min(1, "Title is required").max(100),
  description: z.string().max(500).default(""),
});
 
const updateTaskSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(100).optional(),
  description: z.string().max(500).optional(),
  completed: z.boolean().optional(),
});
 
export const taskRouter = router({
  // GET all tasks
  list: publicProcedure
    .input(
      z
        .object({
          filter: z.enum(["all", "active", "completed"]).default("all"),
        })
        .optional()
    )
    .query(({ input }) => {
      const filter = input?.filter ?? "all";
 
      if (filter === "active") return tasks.filter((t) => !t.completed);
      if (filter === "completed") return tasks.filter((t) => t.completed);
      return tasks;
    }),
 
  // GET single task by ID
  byId: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => {
      const task = tasks.find((t) => t.id === input.id);
      if (!task) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: `Task with ID ${input.id} not found`,
        });
      }
      return task;
    }),
 
  // CREATE a new task
  create: publicProcedure
    .input(createTaskSchema)
    .mutation(({ input }) => {
      const newTask: Task = {
        id: crypto.randomUUID(),
        title: input.title,
        description: input.description,
        completed: false,
        createdAt: new Date(),
        updatedAt: new Date(),
      };
      tasks.push(newTask);
      return newTask;
    }),
 
  // UPDATE an existing task
  update: publicProcedure
    .input(updateTaskSchema)
    .mutation(({ input }) => {
      const index = tasks.findIndex((t) => t.id === input.id);
      if (index === -1) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: `Task with ID ${input.id} not found`,
        });
      }
 
      const updated = {
        ...tasks[index],
        ...input,
        updatedAt: new Date(),
      };
      tasks[index] = updated;
      return updated;
    }),
 
  // DELETE a task
  delete: publicProcedure
    .input(z.object({ id: z.string() }))
    .mutation(({ input }) => {
      const index = tasks.findIndex((t) => t.id === input.id);
      if (index === -1) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: `Task with ID ${input.id} not found`,
        });
      }
      const deleted = tasks.splice(index, 1)[0];
      return deleted;
    }),
});

Remarquez comment chaque entrée est validée avec Zod. Si un client envoie des données invalides, tRPC retourne automatiquement une erreur 400 avec des messages de validation détaillés — et TypeScript détecte l'incompatibilité au moment de la compilation.


Étape 5 : Créer le routeur principal

Combinez tous vos routeurs en un seul routeur racine. Créez src/trpc/routers/_app.ts :

import { router } from "../init";
import { taskRouter } from "./task";
 
export const appRouter = router({
  task: taskRouter,
});
 
// Export the type — this is the magic that enables end-to-end type safety
export type AppRouter = typeof appRouter;

Le type AppRouter est la clé. Vous exportez ce type et l'importez côté client — aucun code d'exécution ne traverse la frontière, uniquement les types. TypeScript infère chaque entrée, sortie et erreur de vos procédures.


Étape 6 : Configurer le gestionnaire de routes

Connectez tRPC à Next.js en utilisant l'adaptateur fetch. Créez src/app/api/trpc/[trpc]/route.ts :

import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { createTRPCContext } from "@/trpc/init";
import { appRouter } from "@/trpc/routers/_app";
 
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: createTRPCContext,
  });
 
export { handler as GET, handler as POST };

Cela crée une route attrape-tout à /api/trpc/*. Chaque procédure tRPC devient automatiquement un endpoint — task.list correspond à /api/trpc/task.list.


Étape 7 : Créer le client tRPC

Maintenant, configurez l'intégration côté client. Vous avez besoin de deux fichiers.

D'abord, créez les hooks tRPC React dans src/trpc/client.ts :

import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "./routers/_app";
 
export const trpc = createTRPCReact<AppRouter>();

Puis créez le provider dans src/trpc/provider.tsx :

"use client";
 
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { useState } from "react";
import { trpc } from "./client";
 
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}`;
}
 
export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 1000,
        retry: 1,
      },
    },
  }));
 
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
        }),
      ],
    })
  );
 
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

Ajoutez le provider à votre layout racine dans src/app/layout.tsx :

import { TRPCProvider } from "@/trpc/provider";
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <TRPCProvider>{children}</TRPCProvider>
      </body>
    </html>
  );
}

Étape 8 : Construire l'interface du gestionnaire de tâches

Maintenant la partie amusante — utiliser tRPC dans vos composants avec une sécurité de type complète.

Créez src/app/page.tsx :

"use client";
 
import { useState } from "react";
import { trpc } from "@/trpc/client";
 
export default function TaskManager() {
  const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
  const [newTitle, setNewTitle] = useState("");
  const [newDescription, setNewDescription] = useState("");
 
  // Queries — fully typed, no manual type annotations needed
  const tasksQuery = trpc.task.list.useQuery({ filter });
 
  // Mutations with automatic cache invalidation
  const utils = trpc.useUtils();
 
  const createTask = trpc.task.create.useMutation({
    onSuccess: () => {
      utils.task.list.invalidate();
      setNewTitle("");
      setNewDescription("");
    },
  });
 
  const updateTask = trpc.task.update.useMutation({
    onSuccess: () => utils.task.list.invalidate(),
  });
 
  const deleteTask = trpc.task.delete.useMutation({
    onSuccess: () => utils.task.list.invalidate(),
  });
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!newTitle.trim()) return;
    createTask.mutate({
      title: newTitle,
      description: newDescription,
    });
  };
 
  return (
    <main className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Task Manager</h1>
 
      {/* Create Task Form */}
      <form onSubmit={handleSubmit} className="mb-8 space-y-4">
        <input
          type="text"
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="Task title..."
          className="w-full p-3 border rounded-lg"
        />
        <textarea
          value={newDescription}
          onChange={(e) => setNewDescription(e.target.value)}
          placeholder="Description (optional)"
          className="w-full p-3 border rounded-lg"
          rows={2}
        />
        <button
          type="submit"
          disabled={createTask.isPending}
          className="px-6 py-3 bg-blue-600 text-white rounded-lg
                     hover:bg-blue-700 disabled:opacity-50"
        >
          {createTask.isPending ? "Adding..." : "Add Task"}
        </button>
      </form>
 
      {/* Filter Tabs */}
      <div className="flex gap-2 mb-6">
        {(["all", "active", "completed"] as const).map((f) => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            className={`px-4 py-2 rounded-lg capitalize ${
              filter === f
                ? "bg-blue-600 text-white"
                : "bg-gray-100 hover:bg-gray-200"
            }`}
          >
            {f}
          </button>
        ))}
      </div>
 
      {/* Task List */}
      {tasksQuery.isLoading && <p>Loading tasks...</p>}
      {tasksQuery.error && (
        <p className="text-red-600">Error: {tasksQuery.error.message}</p>
      )}
 
      <ul className="space-y-3">
        {tasksQuery.data?.map((task) => (
          <li
            key={task.id}
            className="flex items-center gap-4 p-4 border rounded-lg"
          >
            <input
              type="checkbox"
              checked={task.completed}
              onChange={() =>
                updateTask.mutate({
                  id: task.id,
                  completed: !task.completed,
                })
              }
              className="w-5 h-5"
            />
            <div className="flex-1">
              <h3
                className={`font-medium ${
                  task.completed ? "line-through text-gray-400" : ""
                }`}
              >
                {task.title}
              </h3>
              {task.description && (
                <p className="text-sm text-gray-500">{task.description}</p>
              )}
            </div>
            <button
              onClick={() => deleteTask.mutate({ id: task.id })}
              className="text-red-500 hover:text-red-700"
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
 
      {tasksQuery.data?.length === 0 && (
        <p className="text-center text-gray-500 py-8">
          No tasks found. Create one above.
        </p>
      )}
    </main>
  );
}

Remarquez l'autocomplétion. Quand vous tapez trpc.task., votre éditeur affiche list, byId, create, update, delete. Quand vous tapez createTask.mutate({, vous obtenez l'autocomplétion pour title et description avec leurs types exacts. C'est la magie de tRPC — zéro définition de types manuelle côté client.


Étape 9 : Appels côté serveur dans les Server Components

L'une des meilleures fonctionnalités de tRPC est d'appeler des procédures directement depuis les Server Components sans surcharge HTTP.

Créez src/trpc/server.ts :

import { createCallerFactory } from "./init";
import { appRouter } from "./routers/_app";
 
const createCaller = createCallerFactory(appRouter);
 
export const serverTRPC = createCaller({
  userId: null, // populate from session in real apps
});

Maintenant utilisez-le dans un Server Component. Créez src/app/stats/page.tsx :

import { serverTRPC } from "@/trpc/server";
 
export default async function StatsPage() {
  // Direct function call — no HTTP, no serialization overhead
  const allTasks = await serverTRPC.task.list({ filter: "all" });
  const completedTasks = await serverTRPC.task.list({ filter: "completed" });
 
  const completionRate =
    allTasks.length > 0
      ? Math.round((completedTasks.length / allTasks.length) * 100)
      : 0;
 
  return (
    <main className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Task Statistics</h1>
      <div className="grid grid-cols-3 gap-4">
        <div className="p-6 bg-blue-50 rounded-lg text-center">
          <p className="text-3xl font-bold text-blue-600">{allTasks.length}</p>
          <p className="text-gray-600">Total Tasks</p>
        </div>
        <div className="p-6 bg-green-50 rounded-lg text-center">
          <p className="text-3xl font-bold text-green-600">
            {completedTasks.length}
          </p>
          <p className="text-gray-600">Completed</p>
        </div>
        <div className="p-6 bg-purple-50 rounded-lg text-center">
          <p className="text-3xl font-bold text-purple-600">
            {completionRate}%
          </p>
          <p className="text-gray-600">Completion Rate</p>
        </div>
      </div>
    </main>
  );
}

L'appel serverTRPC.task.list() s'exécute directement sur le serveur — même processus, même mémoire, aucun appel réseau. TypeScript applique toujours le contrat complet.


Étape 10 : Ajouter un middleware de journalisation

Le middleware tRPC vous permet d'ajouter des préoccupations transversales comme la journalisation, la limitation de débit ou les analytics.

Mettez à jour src/trpc/init.ts pour ajouter un middleware de journalisation :

// Add this after the existing code in init.ts
 
const logger = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const duration = Date.now() - start;
 
  if (result.ok) {
    console.log(`[tRPC] ${type} ${path} — ${duration}ms OK`);
  } else {
    console.error(`[tRPC] ${type} ${path} — ${duration}ms ERROR`);
  }
 
  return result;
});
 
export const loggedProcedure = t.procedure.use(logger);

Vous pouvez empiler les middlewares. Une procédure peut utiliser à la fois logger et enforceAuth :

export const loggedProtectedProcedure = t.procedure
  .use(logger)
  .use(enforceAuth);

Étape 11 : Bonnes pratiques de gestion des erreurs

tRPC fournit une gestion structurée des erreurs par défaut. Voici comment l'utiliser efficacement.

Lever des erreurs dans les procédures

import { TRPCError } from "@trpc/server";
 
// In your procedure
throw new TRPCError({
  code: "BAD_REQUEST",
  message: "Title cannot be empty",
  cause: originalError, // optional — for debugging
});

Codes d'erreur disponibles

CodeStatut HTTPQuand utiliser
BAD_REQUEST400Entrée invalide
UNAUTHORIZED401Non connecté
FORBIDDEN403Pas de permission
NOT_FOUND404Ressource manquante
CONFLICT409Entrée dupliquée
TOO_MANY_REQUESTS429Limitation de débit
INTERNAL_SERVER_ERROR500Erreur inattendue

Gérer les erreurs côté client

const createTask = trpc.task.create.useMutation({
  onError: (error) => {
    // Zod validation errors
    if (error.data?.zodError) {
      const fieldErrors = error.data.zodError.fieldErrors;
      console.log("Validation errors:", fieldErrors);
      return;
    }
    // tRPC errors
    console.log("Error code:", error.data?.code);
    console.log("Message:", error.message);
  },
});

Étape 12 : Mises à jour optimistes

Pour une interface réactive, vous pouvez mettre à jour le cache avant que le serveur ne réponde :

const updateTask = trpc.task.update.useMutation({
  onMutate: async (newData) => {
    // Cancel outgoing refetches
    await utils.task.list.cancel();
 
    // Snapshot current data
    const previous = utils.task.list.getData({ filter: "all" });
 
    // Optimistically update
    utils.task.list.setData({ filter: "all" }, (old) =>
      old?.map((task) =>
        task.id === newData.id ? { ...task, ...newData } : task
      )
    );
 
    return { previous };
  },
  onError: (_err, _newData, context) => {
    // Rollback on error
    if (context?.previous) {
      utils.task.list.setData({ filter: "all" }, context.previous);
    }
  },
  onSettled: () => {
    utils.task.list.invalidate();
  },
});

Tester votre implémentation

Lancez le serveur de développement :

npm run dev

Ouvrez http://localhost:3000 et vérifiez :

  1. La liste des tâches se charge avec la tâche de départ
  2. Vous pouvez créer de nouvelles tâches avec le formulaire
  3. Cliquer sur la case à cocher bascule l'état de complétion
  4. Le bouton supprimer retire les tâches
  5. Les onglets de filtre fonctionnent correctement
  6. Visitez /stats pour voir les statistiques rendues côté serveur

Tester avec curl

Vous pouvez aussi tester l'API directement :

# List all tasks
curl "http://localhost:3000/api/trpc/task.list?input=%7B%7D"
 
# Create a task
curl -X POST "http://localhost:3000/api/trpc/task.create" \
  -H "Content-Type: application/json" \
  -d '{"json":{"title":"Test from curl","description":"Works!"}}'

Structure du projet

Voici la structure finale du projet :

src/
├── app/
│   ├── api/trpc/[trpc]/
│   │   └── route.ts          # Gestionnaire de route tRPC
│   ├── stats/
│   │   └── page.tsx           # Server Component avec appels côté serveur
│   ├── layout.tsx             # Layout racine avec TRPCProvider
│   └── page.tsx               # Interface du gestionnaire de tâches
└── trpc/
    ├── client.ts              # Hooks React (createTRPCReact)
    ├── init.ts                # Initialisation tRPC, contexte, middleware
    ├── provider.tsx           # Provider côté client
    ├── server.ts              # Appelant côté serveur
    └── routers/
        ├── _app.ts            # Routeur racine
        └── task.ts            # Procédures des tâches

Dépannage

Erreurs "Cannot find module"

Assurez-vous que votre tsconfig.json a les alias de chemin configurés :

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Avertissements "Hydration mismatch"

Assurez-vous que votre TRPCProvider est marqué avec "use client" et n'encapsule que les parties de votre application qui nécessitent les hooks tRPC côté client.

Données obsolètes après les mutations

Appelez toujours utils.task.list.invalidate() dans onSuccess ou onSettled pour récupérer les données après une mutation.

Erreurs de type après modification des procédures

Si vous modifiez l'entrée/sortie d'une procédure, TypeScript peut mettre en cache des types obsolètes. Redémarrez votre serveur TypeScript (Cmd+Shift+P → "TypeScript: Restart TS Server" dans VS Code).


Prochaines étapes

Maintenant que vous avez une configuration tRPC fonctionnelle, envisagez ces améliorations :

  • Ajouter une vraie base de données — remplacez le stockage en mémoire par Drizzle ORM et PostgreSQL
  • Ajouter l'authentification — intégrez AuthJS v5 et renseignez ctx.userId
  • Ajouter des mises à jour en temps réel — utilisez les abonnements tRPC avec WebSockets
  • Ajouter des tests — utilisez createCallerFactory pour les tests unitaires des procédures
  • Déployer — conteneurisez avec Docker ou déployez sur Vercel

Conclusion

tRPC change fondamentalement la façon dont vous construisez des APIs dans les applications TypeScript. Au lieu de maintenir des définitions de types séparées pour votre client et votre serveur, vous écrivez vos procédures une seule fois et laissez TypeScript tout inférer. Le résultat est :

  • Moins de bugs — les incompatibilités de types sont détectées à la compilation, pas en production
  • Développement plus rapide — autocomplétion pour chaque appel API, pas de définitions de types manuelles
  • Moins de code — pas de boilerplate REST, pas de résolveurs GraphQL, pas d'étape de génération de code
  • Meilleure DX — renommez un champ sur le serveur et TypeScript signale immédiatement chaque utilisation côté client

Combiné avec Next.js App Router, vous obtenez le meilleur des deux mondes : le rendu côté serveur avec des appels serveur sans surcharge, et l'interactivité côté client avec une sécurité de type complète. C'est la stack qui rend le développement full-stack TypeScript véritablement fluide.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Découvrez SAM 2 pour une segmentation vidéo précise et performante en traitement d'image.

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 un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·