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.
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 :
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é :
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 :
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 :
- 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’objetopts.req
, puis transmettez la session Ă la fonctioncreateInnerTRPCContext
pour créer le contexte final.
- 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.
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 :
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;
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 :
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
Ressource | Lien |
---|---|
Documentation tRPC | https://www.trpc.io↗ |
Un tas d’exemples de tRPC | https://github.com/trpc/trpc/tree/next/examples↗ |
Documentation React Query | https://tanstack.com/query/v4/docs/adapters/react-query↗ |