Jump to content

tRPC

tRPC nous permet d’écrire des API fortement typées de bout en bout sans aucune génération de code ni surcharge d’exécution. Il utilise l’inférence de TypeScript pour déduire les définitions de type de votre routeur d’API et vous permet d’appeler vos procédures d’API à partir de votre client avec une sécurité de type complète et une saisie semi-automatique dans votre éditeur de code. Lorsque vous utilisez tRPC, vous sentirez votre frontend et votre backend plus proches que jamais, ce qui permet une expérience de développement exceptionnelle.

J'ai écrit tRPC pour permettre aux gens de coder plus rapidement en supprimant le besoin d'une couche API traditionnelle, tout en ayant la certitude que nos applications ne se briseront pas lorsque nous itérerons rapidement.

Avatar of @alexdotjs
Alex - créateur de tRPC @alexdotjs

Comment utiliser tRPC ?

Le contributeur de tRPC trashh_dev↗ a fait une présentation de malade à la Next.js Conf↗ à propos de tRPC. Nous vous recommandons fortement de la regarder si vous ne l’avez pas déjà fait.

Avec tRPC, vous écrivez des fonctions TypeScript sur votre backend, puis vous les appelez depuis votre frontend. Une procédure tRPC simple pourrait ressembler à ceci :

server/api/routers/user.ts
const userRouter = createTRPCRouter({
  getById: publicProcedure.input(z.string()).query(({ ctx, input }) => {
    return ctx.prisma.user.findFirst({
      where: {
        id: input,
      },
    });
  }),
});

Il s’agit d’une procédure tRPC (équivalente à un gestionnaire de route dans un backend traditionnel) qui valide d’abord l’entrée à l’aide de Zod (qui est la même bibliothèque de validation que nous utilisons pour les [variables d’environnement] (./env-variables)) - dans ce cas , il s’assure que l’entrée est une chaîne de caractères. Si l’entrée n’en est pas une, elle renverra une erreur informative à la place.

Après l’entrée, nous enchaînons une fonction de résolveur qui peut être soit une query↗, mutation↗, ou une subscription↗. Dans notre exemple, le résolveur appelle notre base de données à l’aide de notre client prisma et renvoie l’utilisateur dont l’id correspond à celui que nous avons transmis.

Vous définissez vos procédures dans des “routeurs” qui représentent une collection de procédures liées avec un espace de noms partagé. Vous pouvez avoir un routeur pour les utilisateurs, un pour les posts et un autre pour les messages. Ces routeurs peuvent ensuite être fusionnés en un seul appRouter centralisé :

server/api/root.ts
const appRouter = createTRPCRouter({
  users: userRouter,
  posts: postRouter,
  messages: messageRouter,
});

export type AppRouter = typeof appRouter;

Notez que nous n’avons besoin d’exporter que les définitions de type de notre routeur, ce qui signifie que nous n’importons jamais de code serveur sur notre client.

Maintenant appelons la procédure sur notre frontend. tRPC fournit un wrapper pour @tanstack/react-query qui vous permet d’utiliser toute la puissance des hooks qu’il fournit, avec l’avantage supplémentaire d’avoir vos appels d’API typés. Nous pouvons appeler nos procédures depuis notre frontend comme ceci :

pages/users/[id].tsx
import { useRouter } from "next/router";
import { api } from "../../utils/api";

const UserPage = () => {
  const { query } = useRouter();
  const userQuery = api.users.getById.useQuery(query.id);

  return (
    <div>
      <h1>{userQuery.data?.name}</h1>
    </div>
  );
};

Vous remarquerez immédiatement à quel point la saisie semi-automatique et la sécurité de typage sont bonnes. Dès que vous écrivez trpc., vos routeurs s’affichent en saisie semi-automatique et lorsque vous sélectionnez un routeur, ses procédures s’affichent également. Vous obtiendrez également une erreur TypeScript si votre entrée ne correspond pas au validateur que vous avez défini du côté backend.

Fichiers

tRPC nécessite beaucoup de configuration que create-t3-app fait pour vous. Passons en revue les fichiers générés :

đź“„ pages/api/trpc/[trpc].ts

Il s’agit du point d’entrée de votre API et expose le routeur tRPC. Normalement, vous ne toucherez pas beaucoup à ce fichier, mais si vous devez, par exemple, activer le middleware CORS ou similaire, il est utile de savoir que le createNextApiHandler exporté est un gestionnaire d’API Next.js↗ qui prend une requête↗ et réponse↗. Cela signifie que vous pouvez envelopper le createNextApiHandler dans n’importe quel middleware de votre choix. Voir ci-dessous pour un [exemple] (#enabling-cors) d’ajout de CORS.

đź“„ server/api/trpc.ts

Ce fichier est divisé en deux parties, la création du contexte et l’initialisation de tRPC :

  1. Nous définissons le contexte qui est passé à vos procédures tRPC. Le contexte sont des données auxquelles toutes vos procédures tRPC auront accès, et c’est un endroit idéal pour mettre des choses comme les connexions à la base de données, les informations d’authentification, etc. Dans create-t3-app, nous utilisons deux fonctions, pour activer l’utilisation d’un sous-ensemble du contexte lorsque nous n’avons pas accès à l’objet de requête.
  • createInnerTRPCContext : c’est ici que vous dĂ©finissez le contexte qui ne dĂ©pend pas de la requĂŞte, par ex. votre connexion Ă  la base de donnĂ©es. Vous pouvez utiliser cette fonction pour les tests d’intĂ©gration ou ssg-helpers↗ oĂą vous n’avez pas d’objet de requĂŞte .

  • createTRPCContext : c’est ici que vous dĂ©finissez le contexte qui dĂ©pend de la requĂŞte, par ex. la session de l’utilisateur. Vous demandez la session Ă  l’aide de l’objet opts.req, puis transmettez la session Ă  la fonction createInnerTRPCContext pour crĂ©er le contexte final.

  1. Nous initialisons tRPC et définissons des procédures↗ et des middlewares↗ réutilisables. Par convention, vous ne devriez pas exporter l’intégralité de l’objet t, mais plutôt de créer des procédures et des middlewares réutilisables et de les exporter.

Vous remarquerez que nous utilisons superjson comme transformateur de données↗. Cela fait en sorte que vos types de données sont préservés lorsqu’ils atteignent le client, donc si vous envoyez par exemple un objet Date, le client renverra une Date et non une chaîne, ce qui est le cas pour la plupart des API.

đź“„ server/api/routers/*.ts

C’est ici que vous définissez les routes et les procédures de votre API. Par convention, vous créez des routeurs séparés↗ pour les procédures associées.

đź“„ server/api/root.ts

Ici, nous fusionnons↗ tous les sous-routeurs définis dans routers/** en un seul routeur d’application.

đź“„ utils/api.ts

Il s’agit du point d’entrée frontend pour tRPC. C’est ici que vous allez importer la définition de type du routeur et créer votre client tRPC avec les hooks de react-query. Depuis que nous avons activé superjson comme transformateur de données sur le backend, nous devons également l’activer sur le frontend. En effet, les données sérialisées du backend sont désérialisées sur le frontend.

Vous définirez ici vos liens↗ tRPC, qui détermine le flux de requêtes du client vers le serveur. Nous utilisons le httpBatchLink↗ “par défaut” qui active le traitement par lot des requêtes↗, ainsi qu’un loggerLink↗ qui génère des journaux de requêtes utiles pendant le développement.

Enfin, nous exportons un helper de type↗ que vous pouvez utiliser pour déduire vos types sur le frontend.

Le contributeur de Create T3 App Christopher Ehrlich↗ a réalisé une vidéo sur les flux de données dans tRPC↗. Cette vidéo est recommandée si vous avez utilisé tRPC mais que vous ne savez toujours pas comment cela fonctionne.

Comment puis-je appeler mon API en externe ?

Avec les API classiques, vous pouvez appeler vos points de terminaison à l’aide de n’importe quel client HTTP tel que curl, Postman, fetch ou directement depuis votre navigateur. Avec tRPC, c’est un peu différent. Si vous souhaitez appeler vos procédures sans le client tRPC, il existe deux méthodes recommandées :

Exposez une seule procédure vers l’extérieur

Si vous souhaitez exposer une seule procédure vers l’extérieur, vous cherchez des appels côté serveur↗. Cela vous permettrait de créer un point de terminaison API Next.js normal, et de réutiliser la partie résolveur de votre procédure tRPC.

pages/api/users/[id].ts
import { type NextApiRequest, type NextApiResponse } from "next";
import { appRouter, createCaller } from "../../../server/api/root";
import { createTRPCContext } from "../../../server/api/trpc";

const userByIdHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  // Create context and caller
  const ctx = await createTRPCContext({ req, res });
  const caller = createCaller(ctx);
  try {
    const { id } = req.query;
    const user = await caller.user.getById(id);
    res.status(200).json(user);
  } catch (cause) {
    if (cause instanceof TRPCError) {
      // An error from tRPC occurred
      const httpCode = getHTTPStatusCodeFromError(cause);
      return res.status(httpCode).json(cause);
    }
    // Another error occurred
    console.error(cause);
    res.status(500).json({ message: "Internal server error" });
  }
};

export default userByIdHandler;

Exposer chaque procédure en tant que point de terminaison REST

Si vous souhaitez exposer chaque procédure vers l’extérieur, consultez le plugin créer par la communauté trpc-openapi↗. En fournissant des métadonnées supplémentaires à vos procédures, vous pouvez générer une API REST compatible OpenAPI à partir de votre routeur tRPC.

Ce ne sont que des requĂŞtes HTTP

tRPC communique via HTTP, il est donc également possible d’appeler vos procédures tRPC à l’aide de requêtes HTTP “régulières”. Cependant, la syntaxe peut être fastidieuse en raison du protocole RPC↗ utilisé par tRPC. Si vous êtes curieux, vous pouvez regarder à quoi ressemblent les demandes et les réponses tRPC dans l’onglet réseau de votre navigateur, mais nous vous suggérons de le faire uniquement à titre d’exercice pédagogique et de vous en tenir à l’une des solutions décrites ci-dessus.

Comparaison avec un endpoint d’API Next.js

Comparons un endpoint d’API Next.js à une procédure tRPC. Disons que nous voulons récupérer un objet utilisateur de notre base de données et le renvoyer au frontend. Nous pourrions écrire un endpoint d’API Next.js comme ceci :

pages/api/users/[id].ts
import { type NextApiRequest, type NextApiResponse } from "next";
import { prisma } from "../../../server/db";

const userByIdHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== "GET") {
    return res.status(405).end();
  }

  const { id } = req.query;

  if (!id || typeof id !== "string") {
    return res.status(400).json({ error: "Invalid id" });
  }

  const examples = await prisma.example.findFirst({
    where: {
      id,
    },
  });

  res.status(200).json(examples);
};

export default userByIdHandler;
pages/users/[id].tsx
import { useState, useEffect } from "react";
import { useRouter } from "next/router";

const UserPage = () => {
  const router = useRouter();
  const { id } = router.query;

  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/user/${id}`)
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, [id]);
};

Comparez cela à l’exemple tRPC ci-dessus et vous pouvez voir certains des avantages de tRPC :

  • Au lieu de spĂ©cifier une url pour chaque route, ce qui peut devenir fastidieux Ă  dĂ©boguer si vous dĂ©placez quelque chose, votre routeur entier est un objet avec saisie semi-automatique.
  • Vous n’avez pas besoin de valider la mĂ©thode HTTP utilisĂ©e.
  • Vous n’avez pas besoin de valider que la requĂŞte ou le corps de la requĂŞte contient les donnĂ©es correctes dans la procĂ©dure, car Zod s’en charge.
  • Au lieu de crĂ©er une rĂ©ponse, vous pouvez gĂ©nĂ©rer des erreurs et renvoyer une valeur ou un objet comme vous le feriez dans n’importe quelle autre fonction TypeScript.
  • L’appel de la procĂ©dure sur le frontend fournit l’auto-complĂ©tion et la sĂ©curitĂ© de typage.

Extraits de code utiles

Voici quelques extraits de code qui pourraient ĂŞtre utiles.

Activation de CORS

Si vous avez besoin de consommer votre API à partir d’un domaine différent, par exemple dans un monorepo qui inclut une application React Native, vous devrez peut-être activer CORS :

pages/api/trpc/[trpc].ts
import { type NextApiRequest, type NextApiResponse } from "next";
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import cors from "nextjs-cors";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  // Enable cors
  await cors(req, res);

  // Create and call the tRPC handler
  return createNextApiHandler({
    router: appRouter,
    createContext: createTRPCContext,
  })(req, res);
};

export default handler;

Mises Ă  jour optimistes

Les mises à jour optimistes se produisent lorsque nous mettons à jour l’interface utilisateur avant la fin de l’appel d’API. Cela donne à l’utilisateur une meilleure expérience car il n’a pas à attendre la fin de l’appel d’API pour que l’interface utilisateur reflète le résultat de son action. Cependant, les applications qui accordent une grande importance à l’exactitude des données doivent éviter les mises à jour optimistes car elles ne sont pas une “véritable” représentation de l’état du backend. Vous pouvez en savoir plus sur la documentation de React Query↗.

const MyComponent = () => {
  const listPostQuery = api.post.list.useQuery();

  const utils = api.useContext();
  const postCreate = api.post.create.useMutation({
    async onMutate(newPost) {
      // Cancel outgoing fetches (so they don't overwrite our optimistic update)
      await utils.post.list.cancel();

      // Get the data from the queryCache
      const prevData = utils.post.list.getData();

      // Optimistically update the data with our new post
      utils.post.list.setData(undefined, (old) => [...old, newPost]);

      // Return the previous data so we can revert if something goes wrong
      return { prevData };
    },
    onError(err, newPost, ctx) {
      // If the mutation fails, use the context-value from onMutate
      utils.post.list.setData(undefined, ctx.prevData);
    },
    onSettled() {
      // Sync with server once mutation has settled
      utils.post.list.invalidate();
    },
  });
};

Exemple de test d’intégration

Voici un exemple de test d’intégration qui utilise Vitest↗ pour vérifier que votre routeur tRPC fonctionne comme prévu, que l’analyseur d’entrée déduit le type correct et que les données renvoyées correspondent à la sortie attendue.

import { type inferProcedureInput } from "@trpc/server";
import { expect, test } from "vitest";

import { appRouter, type AppRouter } from "~/server/api/root";
import { createInnerTRPCContext } from "~/server/api/trpc";

test("example router", async () => {
  const ctx = await createInnerTRPCContext({ session: null });
  const caller = appRouter.createCaller(ctx);

  type Input = inferProcedureInput<AppRouter["example"]["hello"]>;
  const input: Input = {
    text: "test",
  };

  const example = await caller.example.hello(input);

  expect(example).toMatchObject({ greeting: "Hello test" });
});

Ressources utiles

RessourceLien
Documentation tRPChttps://www.trpc.io↗
Un tas d’exemples de tRPChttps://github.com/trpc/trpc/tree/next/examples↗
Documentation React Queryhttps://tanstack.com/query/v4/docs/adapters/react-query↗