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.
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:
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
:
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:
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:
- 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 Ă„ brukeopts.req
-objektet, og sender deretter Ăžkten ned tilcreateInnerTRPCContext
-funksjonen for Ă„ lage den endelige konteksten.
- 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.
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:
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]);
};
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:
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
Ressurser | Link |
---|---|
tRPC Dokumentasjon | https://www.trpc.ioâ |
Noen tRPC-eksempler | https://github.com/trpc/trpc/tree/next/examplesâ |
React Query Dokumentasjon | https://tanstack.com/query/v4/docs/adapters/react-queryâ |