Jump to content

tRPC

tRPC lar oss skrive ende-til-ende typesikre APIer, helt uten kodegenerering eller runtime-bloat. tRPC bruker TypeScripts inferens for Ä automatisk utlede API-ruterens typedefinisjoner og lar deg kalle API-prosedyrene dine fra frontend med full typesikkerhet og autofullfÞring. NÄr du bruker tRPC, fÞles frontend og backend nÊrmere enn noen gang, noe som resulterer i en enestÄende utvikleropplevelse.

"Jeg bygde tRPC for Ä forbedre hastigheten pÄ utviklingen av applikasjoner ved Ä fjerne behovet for et tradisjonelt API-lag. Samtidig kan vi fortsatt stole pÄ at de vil vÊre stabile nÄr man itererer raskt.

Avatar of @alexdotjs
Alex - creator of tRPC @alexdotjs

Hvordan bruker jeg tRPC?

tRPC-bidragsyter trashh_dev↗ holdt en flott tale pĂ„ Next.js Conf↗ om tRPC. Vi anbefaler deg Ă„ se den hvis du ikke allerede har gjort det.

Med tRPC skriver du TypeScript-funksjoner i backend, og kaller dem deretter fra frontend. En enkel tRPC-prosedyre kan se slik ut:

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

Dette er tRPC-prosedyre (tilsvarer en rutebehandler i en tradisjonell backend) som fĂžrst validerer inndataene ved Ă„ bruke Zod (som er det samme valideringsbiblioteket vi bruker for miljĂžvariablene) - i dette tilfellet forsikres det at input er en streng. Hvis input ikke er en streng, returneres en detaljert feil.

Etter input fĂžlger en resolver-funksjon som enten utfĂžrer en query↗, mutasjon↗ eller en subscription↗. I vĂ„rt eksempel kaller resolver-funksjonen vĂ„r database med vĂ„r prisma-klient og returnerer brukeren hvis id samsvarer med den vi sendte inn.

Du definerer prosedyrene dine i rutere som er en samling av relaterte prosedyrer innenfor et felles namespace. Du kan ha en ruter for users, en for posts og en for messages. Disse ruterne kan deretter slÄs sammen til en enkelt, sentral appRouter:

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

export type AppRouter = typeof appRouter;

Merk at vi bare trenger Ä eksportere vÄr ruters typedefinisjoner, noe som betyr at vi aldri importerer noen serverkode i klienten vÄr.

La oss nĂ„ pĂ„kalle prosedyren i frontenden vĂ„r. tRPC tilbyr en wrapper for @tanstack/react-query hvor det er definert noen hooks som gjĂžr at du kan pĂ„kalle ditt API med definerte typer som er “inferred”, det vil at TypeScript-kompilatoren automatisk har utledet hvilken type API-kallene dine har. Vi kan kalle prosedyrene vĂ„re fra vĂ„r frontend slik:

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>
  );
};

Du vil umiddelbart legge merke til hvor god autofullfÞringen og typesikkerheten er. SÄ snart du skriver inn api. vil ruterne dine automatisk bli foreslÄtt. Hvis du nÄ velger en ruter, vil prosedyrene ogsÄ vises. Hvis inndataene dine ikke samsvarer med validatoren du definerte i backend, fÄr du en TypeScript-feil.

Filer

tRPC krever mye boilerplate, som create-t3-app setter opp for deg. La oss gÄ gjennom filene som vil bli opprettet:

📄 pages/api/trpc/[trpc].ts

Dette er inngangspunktet for API-et ditt og eksponerer tRPC-ruteren. Normalt vil du ikke vĂŠre borti denne filen sĂ„ ofte. Men hvis du f.eks. trenger en middleware for CORS eller lignende, er det nyttig Ă„ vite at den eksporterte funksjonen createNextApiHandler er en Next.js API-Handler↗ som mottar et request-↗ og et response↗-objekt. Dette betyr at du kan wrappe createNextApiHandler med hvilken som helst middleware. Se under for et eksempel for Ă„ legge til CORS.

📄 server/api/trpc.ts

Denne filen er delt opp i to deler, kontekstoppretting og tRPC-initialisering:

  1. Vi definerer konteksten som videresendes til tRPC-prosedyrene dine. Kontekst er data som alle dine tRPC-prosedyrer vil ha tilgang til, og er et flott sted Ä plassere ting som databaseforbindelser, autentiseringsinformasjon, osv. I create-t3-app bruker vi to funksjoner, for Ä muliggjÞre bruk av en undergruppe av konteksten nÄr vi ikke har tilgang til forespÞrselsobjektet.
  • createInnerTRPCContext: Det er her du definerer konteksten som ikke avhenger av forespĂžrselen, f.eks. databasetilkoblingen din. Du kan bruke denne funksjonen til integrasjonstesting eller ssg-hjelpere↗ der du ikke har et forespĂžrselsobjekt.

  • createTRPCContext: Det er her du definerer konteksten som avhenger av forespĂžrselen, f.eks. brukerens Ăžkt. Du ber om Ăžkten ved Ă„ bruke opts.req-objektet, og sender deretter Ăžkten ned til createInnerTRPCContext-funksjonen for Ă„ lage den endelige konteksten.

  1. Vi initialiserer tRPC og definerer gjenbrukbare prosedyrer↗ og middlewares↗. Av konvensjon bþr du ikke eksponere hele t-objektet, men i stedet lage gjenbrukbare prosedyrer og middleware og eksportere de.

Du har sikkert lagt merke til at vi bruker superjson som datatransformator↗. Dette sikrer at datatypene dine blir bevart nĂ„r de nĂ„r klienten, sĂ„ hvis du for eksempel sender et Date-objekt til klienten sĂ„ returneres et Date-objekt og ikke en streng slik de fleste API gjĂžr.

📄 server/api/routers/*.ts

Det er her du definerer ruterne og prosedyrene for API-et din. Konvensjon tilsier at du bþr opprette separate rutere↗ for relaterte prosedyrer.

📄 server/api/root.ts

Her slĂ„r vi sammen alle underruterne definert i routers/** merge↗ til et enkelt app-ruter.

📄 utils/api.ts

Dette er inngangspunktet for tRPC pÄ klientsiden. Her importerer du ruterens typedefinisjonen og oppretter tRPC-klienten, samt hooks for react-query. Ettersom vi har aktivert superjson som vÄr datatransformator pÄ serversiden, mÄ vi aktivere den pÄ klientsiden ogsÄ. Dette er fordi serialisert data fra backend blir deserialisert pÄ frontend.

Du definerer tRPC lenker↗ her, som kartlegger request-flyten fra klienten til serveren. Vi bruker “standard” httpBatchLink↗ som muliggjþr request batching↗, samt en loggerLink↗ som gir ut request-logger som kan vére nyttige under utviklingsprosessen.

Til slutt eksporterer vi en hjelpertype↗ som du kan bruke til Ă„ utlede typene dine pĂ„ klientsiden.

Hvordan pÄkaller jeg API-et mitt eksternt?

Med vanlige API-er kan du bruke hvilken som helst HTTP-klient som curl, Postman, fetch eller bare nettleseren din. Med tRPC er det litt annerledes. Hvis du vil kalle opp prosedyrene dine uten tRPC-klienten, er det to anbefalte mÄter:

GjĂžr en enkelt prosedyre tilgjengelig eksternt

Hvis du Ăžnsker Ă„ eksponere en enkel prosedyre eksternt, er du avhengig av server-side-kall↗. Dette vil tillate deg Ă„ opprette et normalt Next.js API-endepunkt, men gjenbruke resolver-delen av tRPC-prosedyren.

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;

Vis alle prosedyrer som et REST-endepunkt

Hvis du vil gjĂžre hver enkelt prosedyre tilgjengelig eksternt, sjekk ut den community-skapte plugin-modulen trpc-openapi↗. Den lar deg generere et OpenAPI-kompatibelt REST API fra tRPC-ruteren din, ved Ă„ legge til ytterligere metadata i prosedyrene dine.

Dette er kun HTTP-requests

tRPC kommuniserer via HTTP, sĂ„ det er ogsĂ„ mulig Ă„ starte tRPC-prosedyrene dine med “normale” HTTP-requests. Syntaksen kan imidlertid vĂŠre vanskelig pĂ„ grunn av RPC-protokollen↗ som tRPC bruker. Hvis du er nysgjerrig, sjekk nettleserens nettverksfane for Ă„ se hvordan tRPC-requestene og -responsene ser ut. Vi anbefaler imidlertid dette kun for pedagogiske formĂ„l og vil rĂ„de deg til generelt Ă„ bruke en av lĂžsningene ovenfor.

Sammenligning med et Next.js API-endepunkt

La oss sammenligne et Next.js API-endepunkt med en tRPC-prosedyre. Anta at vi Þnsker Ä hente et brukerobjekt fra databasen vÄr og returnere det til frontend. Vi kan skrive et Next.js API-endepunkt slik:

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]);
};

Hvis vi nÄ sammenligner dette med tRPC-eksemplet fra lenger opp i dokumentasjonen, kan fÞlgende fordeler med tRPC sees:

  • I stedet for Ă„ spesifisere en URL for hver rute, som kan forĂ„rsake feil ved endring av prosjektets struktur, er hele ruteren et objekt med autofullfĂžring.
  • Du trenger ikke Ă„ validere hvilken HTTP-metode som ble brukt.
  • Du trenger ikke Ă„ validere at request eller body inneholder riktige data i prosedyren, fordi Zod tar seg av dette.
  • I stedet for Ă„ opprette en response, kan du kaste en error og returnere en verdi eller et objekt som du ville gjort i en hvilken som helst annen TypeScript-funksjon.
  • Å kalle prosedyren pĂ„ frontend gir autofullfĂžring og typesikkerhet.

Nyttige Kodeutdrag

Her er noen kodesnutter som kan hjelpe deg.

Aktivering av CORS

Hvis du trenger Ä konsumere API-et ditt fra et annet domene, for eksempel i en monorepo som inneholder en React Native-app, mÄ du antageligvis aktivere 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) => {
  // Aktiver cors
  await cors(req, res);

  // Oprett og pÄkall tRPC-handler
  return createNextApiHandler({
    router: appRouter,
    createContext: createTRPCContext,
  })(req, res);
};

export default handler;

Optimistiske oppdateringer

Optimistiske oppdateringer er oppdateringer vi gjĂžr fĂžr API-forespĂžrselen fullfĂžres. Dette gir en bedre opplevelse for brukeren siden de ikke trenger Ă„ vente pĂ„ at API-forespĂžrselen skal fullfĂžres fĂžr brukergrensesnittet reflekterer resultatet av handlingen deres. Imidlertid bĂžr applikasjoner som verdsetter riktigheten av dataen unngĂ„ optimistiske oppdateringer, da de ikke gjenspeiler de “sanne” dataene til backend. Du kan lese mer om det i React Query-dokumentasjonen↗.

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

   const utils = api.useContext();
   const postCreate = api.post.create.useMutation({
     async onMutate(newPost) {
       // Avbryt utgÄende henting (slik at de ikke overskriver vÄr optimistiske oppdatering)
       vent utils.post.list.cancel();

       // FĂ„ dataene fra queryCache
       const prevData = utils.post.list.getData();

       // Oppdater dataene optimistisk med vÄrt nye innlegg
       utils.post.list.setData(udefinert, (gammel) => [...gammel, nyinnlegg]);

       // Returner forrige data slik at vi kan gÄ tilbake hvis noe gÄr galt
       return { prevData };
     },
     onError(err, newPost, ctx) {
       // Hvis mutasjonen mislykkes, bruk kontekstverdien fra onMutate
       utils.post.list.setData(udefinert, ctx.prevData);
     },
     onSettled() {
       // Synkroniser med server nÄr mutasjonen er fullfÞrt
       utils.post.list.invalidate();
     },
   });
};

Eksempel pÄ Integrasjonstest

Her er et eksempel pĂ„ en integrasjonstest som bruker Vitest↗ for Ă„ bekrefte at tRPC-ruteren din fungerer som forventet, at input-parseren inferrer riktig type, og at returnert data samsvarer med forventet output.

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" });
});

Nyttige Ressurser

RessurserLink
tRPC Dokumentasjonhttps://www.trpc.io↗
Noen tRPC-eksemplerhttps://github.com/trpc/trpc/tree/next/examples↗
React Query Dokumentasjonhttps://tanstack.com/query/v4/docs/adapters/react-query↗