Använda Server Actions med tRPC
Denna sida har översatts av PageTurner AI (beta). Inte officiellt godkänd av projektet. Hittade du ett fel? Rapportera problem →
Byggarmönstret för att skapa procedurer som introducerades i tRPC v10 har fått stor uppskattning från communityt, och många bibliotek har antagit liknande mönster.
Det har till och med myntats en term tRPC like XYZ som bevis på mönstrets ökande popularitet. För inte så länge sedan såg jag faktiskt någon
som undrade om det fanns ett sätt att skriva CLI-applikationer med ett liknande API som tRPC.
Sidnotering: du kan till och med använda tRPC direkt för detta. Men det är inte det vi ska prata om idag,
vi ska diskutera hur man använder tRPC med server actions från Next.js.
Vad är en server action?
Om du levt under en sten och inte hängt med i de senaste React- och Next.js-funktionerna: server actions låter dig skriva vanliga funktioner som körs på servern, importera dem på klienten och anropa dem precis som om de vore vanliga funktioner. Du kanske tycker att detta låter likt tRPC - och det stämmer. Enligt Dan Abramov är server actions "tRPC som en bundler-funktion":
Och detta är helt korrekt. Server actions liknar tRPC, i slutändan är de båda RPC:er. Båda låter dig skriva funktioner på backend och anropa dem med full typstyrning på frontend med nätverkslagret abstraherat bort.
Så var kommer tRPC in i bilden? Varför skulle jag behöva både tRPC och server actions? Server actions är en primitiv, och som alla primitiver är de ganska grundläggande och saknar därmed vissa fundamentala aspekter när det gäller att bygga API:er. För alla API-slutpunkter som exponeras över nätverket måste du validera och auktorisera förfrågningar för att säkerställa att ditt API inte missbrukas. Som tidigare nämnts uppskattas tRPC:s API av communityt, så vore det inte trevligt om vi kunde använda tRPC för att definiera server actions och utnyttja alla fantastiska funktioner som kommer inbyggda i tRPC? Tänk validering av indata, autentisering och auktorisering via middleware, validering av utdata, datatransformatorer, etc. Jag tycker det, så låt oss gräva djupare.
Definiera server actions med tRPC
Förutsättningar: För att använda server actions måste du använda Next.js App Router. Dessutom är alla tRPC-funktioner vi ska använda endast tillgängliga i tRPC v11, så se till att du använder beta-releasekanalen för tRPC:
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server
yarn add @trpc/server
pnpm add @trpc/server
bun add @trpc/server
deno add npm:@trpc/server
Låt oss börja med att initiera tRPC och definiera vår basprocedur för server actions.
Vi kommer använda metoden experimental_caller i procedure-builder:en, en ny metod som låter dig
anpassa hur proceduren anropas när den invokeras som en funktion. Vi kommer också använda adapter:en experimental_nextAppDirCaller
för att göra den kompatibel med Next.js. Denna adapter hanterar fall där server action:en är inlindad i useActionState på klienten,
vilket ändrar anropssignaturen för server action:en.
Vi kommer också använda en span-egenskap som metadata, eftersom det inte finns någon vanlig sökväg som när du använder en router (t.ex. user.byId). Du kan använda span-egenskapen för att skilja mellan procedurer, till exempel vid loggning eller observabilitet.
server/trpc.tstsimport {initTRPC ,TRPCError } from '@trpc/server';import {experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';interfaceMeta {span : string;}export constt =initTRPC .meta <Meta >().create ();constserverActionProcedure =t .procedure .experimental_caller (experimental_nextAppDirCaller ({pathExtractor : ({meta }) => (meta asMeta ).span ,}),);
server/trpc.tstsimport {initTRPC ,TRPCError } from '@trpc/server';import {experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';interfaceMeta {span : string;}export constt =initTRPC .meta <Meta >().create ();constserverActionProcedure =t .procedure .experimental_caller (experimental_nextAppDirCaller ({pathExtractor : ({meta }) => (meta asMeta ).span ,}),);
Därefter lägger vi till kontext. Eftersom vi inte kommer hosta en router med en vanlig HTTP-adapter kommer vi inte få någon kontext injicerad genom createContext-metoden på adapter:en. Istället använder vi en middleware för att injicera vår kontext. I detta exempel hämtar vi den aktuella användaren från sessionen och injicerar den i kontexten.
server/trpc.tstsimport {initTRPC ,TRPCError } from '@trpc/server';import {experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';import {currentUser } from './auth';interfaceMeta {span : string;}export constt =initTRPC .meta <Meta >().create ();export constserverActionProcedure =t .procedure .experimental_caller (experimental_nextAppDirCaller ({pathExtractor : ({meta }) => (meta asMeta ).span ,}),).use (async (opts ) => {// Inject user into contextconstuser = awaitcurrentUser ();returnopts .next ({ctx : {user } });});
server/trpc.tstsimport {initTRPC ,TRPCError } from '@trpc/server';import {experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';import {currentUser } from './auth';interfaceMeta {span : string;}export constt =initTRPC .meta <Meta >().create ();export constserverActionProcedure =t .procedure .experimental_caller (experimental_nextAppDirCaller ({pathExtractor : ({meta }) => (meta asMeta ).span ,}),).use (async (opts ) => {// Inject user into contextconstuser = awaitcurrentUser ();returnopts .next ({ctx : {user } });});
Slutligen skapar vi en protectedAction-procedur som skyddar alla åtgärder från oautentiserade användare. Om du redan har en middleware som gör detta kan du använda den, men jag definierar en inline i det här exemplet.
server/trpc.tstsimport {initTRPC ,TRPCError } from '@trpc/server';import {experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';import {currentUser } from './auth';interfaceMeta {span : string;}export constt =initTRPC .meta <Meta >().create ();export constserverActionProcedure =t .procedure .experimental_caller (experimental_nextAppDirCaller ({pathExtractor : ({meta }) => (meta asMeta ).span ,}),).use (async (opts ) => {// Inject user into contextconstuser = awaitcurrentUser ();returnopts .next ({ctx : {user } });});export constprotectedAction =serverActionProcedure .use ((opts ) => {if (!opts .ctx .user ) {throw newTRPCError ({code : 'UNAUTHORIZED',});}returnopts .next ({ctx : {...opts .ctx ,user :opts .ctx .user , // <-- ensures type is non-nullable},});});
server/trpc.tstsimport {initTRPC ,TRPCError } from '@trpc/server';import {experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';import {currentUser } from './auth';interfaceMeta {span : string;}export constt =initTRPC .meta <Meta >().create ();export constserverActionProcedure =t .procedure .experimental_caller (experimental_nextAppDirCaller ({pathExtractor : ({meta }) => (meta asMeta ).span ,}),).use (async (opts ) => {// Inject user into contextconstuser = awaitcurrentUser ();returnopts .next ({ctx : {user } });});export constprotectedAction =serverActionProcedure .use ((opts ) => {if (!opts .ctx .user ) {throw newTRPCError ({code : 'UNAUTHORIZED',});}returnopts .next ({ctx : {...opts .ctx ,user :opts .ctx .user , // <-- ensures type is non-nullable},});});
Okej, låt oss skriva en riktig serveråtgärd. Skapa en _actions.ts-fil, dekorera den med direktivet "use server", och definiera din åtgärd.
app/_actions.tsts'use server';import {z } from 'zod';import {protectedAction } from '../server/trpc';export constcreatePost =protectedAction .input (z .object ({title :z .string (),}),).mutation (async (opts ) => {// Do something with the input});// Since we're using the `experimental_caller`,// our procedure is now just an ordinary function:createPost ;
app/_actions.tsts'use server';import {z } from 'zod';import {protectedAction } from '../server/trpc';export constcreatePost =protectedAction .input (z .object ({title :z .string (),}),).mutation (async (opts ) => {// Do something with the input});// Since we're using the `experimental_caller`,// our procedure is now just an ordinary function:createPost ;
Wow, så enkelt är det att definiera en serveråtgärd som är skyddad från oautentiserade användare, med indatavalidering för att skydda mot attacker som SQL-injektioner. Låt oss importera den här funktionen på klienten och anropa den.
app/post-form.tsxtsx'use client';import { createPost } from '~/_actions';export function PostForm() {return (<form// Use `action` to make form progressively enhancedaction={createPost}// `Using `onSubmit` allows building rich interactive// forms once JavaScript has loadedonSubmit={async (e) => {e.preventDefault();const title = new FormData(e.target).get('title');// Maybe show loading toast, etc etc. Endless possibilitiesawait createPost({ title });}}><input type="text" name="title" /><button type="submit">Create Post</button></form>);}
app/post-form.tsxtsx'use client';import { createPost } from '~/_actions';export function PostForm() {return (<form// Use `action` to make form progressively enhancedaction={createPost}// `Using `onSubmit` allows building rich interactive// forms once JavaScript has loadedonSubmit={async (e) => {e.preventDefault();const title = new FormData(e.target).get('title');// Maybe show loading toast, etc etc. Endless possibilitiesawait createPost({ title });}}><input type="text" name="title" /><button type="submit">Create Post</button></form>);}
Vidareutveckling
Genom att använda tRPC:s byggstenar och dess komponerbara sätt att definiera återanvändbara procedurer kan vi enkelt skapa mer komplexa serveråtgärder. Här är några exempel:
Observabilitet
Du kan använda @baselime/node-opentelemtry:s tRPC-plugin för att lägga till observabilitet med bara några rader kod:
diff--- server/trpc.ts+++ server/trpc.ts+ import { tracing } from '@baselime/node-opentelemetry/trpc';export const serverActionProcedure = t.procedure.experimental_caller(experimental_nextAppDirCaller({pathExtractor: (meta: Meta) => meta.span,}),).use(async (opts) => {// Inject user into contextconst user = await currentUser();return opts.next({ ctx: { user } });})+ .use(tracing());--- app/_actions.ts+++ app/_actions.tsexport const createPost = protectedAction+ .meta({ span: 'create-post' }).input(z.object({title: z.string(),}),).mutation(async (opts) => {// Do something with the input});
diff--- server/trpc.ts+++ server/trpc.ts+ import { tracing } from '@baselime/node-opentelemetry/trpc';export const serverActionProcedure = t.procedure.experimental_caller(experimental_nextAppDirCaller({pathExtractor: (meta: Meta) => meta.span,}),).use(async (opts) => {// Inject user into contextconst user = await currentUser();return opts.next({ ctx: { user } });})+ .use(tracing());--- app/_actions.ts+++ app/_actions.tsexport const createPost = protectedAction+ .meta({ span: 'create-post' }).input(z.object({title: z.string(),}),).mutation(async (opts) => {// Do something with the input});
Kolla in Baselimes tRPC-integrering för mer information. Liknande mönster bör fungera oavsett vilken observabilitetsplattform du använder.
Begränsning av anrop (Rate Limiting)
Du kan använda en tjänst som Unkey för att begränsa antalet anrop till dina serveråtgärder. Här är ett exempel på en skyddad serveråtgärd som använder Unkey för att begränsa antalet förfrågningar per användare:
server/trpc.tstsimport {Ratelimit } from '@unkey/ratelimit';export constrateLimitedAction =protectedAction .use (async (opts ) => {constunkey = newRatelimit ({rootKey :process .env .UNKEY_ROOT_KEY !,async : true,duration : '10s',limit : 5,namespace : `trpc_${opts .path }`,});constratelimit = awaitunkey .limit (opts .ctx .user .id );if (!ratelimit .success ) {throw newTRPCError ({code : 'TOO_MANY_REQUESTS',message :JSON .stringify (ratelimit ),});}returnopts .next ();});
server/trpc.tstsimport {Ratelimit } from '@unkey/ratelimit';export constrateLimitedAction =protectedAction .use (async (opts ) => {constunkey = newRatelimit ({rootKey :process .env .UNKEY_ROOT_KEY !,async : true,duration : '10s',limit : 5,namespace : `trpc_${opts .path }`,});constratelimit = awaitunkey .limit (opts .ctx .user .id );if (!ratelimit .success ) {throw newTRPCError ({code : 'TOO_MANY_REQUESTS',message :JSON .stringify (ratelimit ),});}returnopts .next ();});
app/_actions.tsts'use server';import {z } from 'zod';import {rateLimitedAction } from '../server/trpc';export constcommentOnPost =rateLimitedAction .input (z .object ({postId :z .string (),content :z .string (),}),).mutation (async (opts ) => {console .log (`${opts .ctx .user .name } commented on ${opts .input .postId } saying ${opts .input .content }`,);});
app/_actions.tsts'use server';import {z } from 'zod';import {rateLimitedAction } from '../server/trpc';export constcommentOnPost =rateLimitedAction .input (z .object ({postId :z .string (),content :z .string (),}),).mutation (async (opts ) => {console .log (`${opts .ctx .user .name } commented on ${opts .input .postId } saying ${opts .input .content }`,);});
Läs mer om att begränsa anrop till dina tRPC-procedurer i det här inlägget från Unkey.
Möjligheterna är oändliga, och jag slår vad om att du redan har en massa bra verktygsmiddlewares som du använder i dina tRPC-applikationer idag. Om inte, kanske du hittar några du kan npm installa!
Avslutningsvis
Serveråtgärder är inget silverkul. I situationer som kräver mer dynamisk data kanske du vill behålla din data i React Queries klient-sidcache och göra mutationer med useMutation istället. Det är helt legitimt. Dessa nya primitiv bör också vara lätta att adoptera gradvis, så du kan flytta individuella procedurer från din befintliga tRPC-API till serveråtgärder där det är meningsfullt. Det finns inget behov av att skriva om hela ditt API.
Genom att definiera dina serveråtgärder med tRPC kan du återanvända mycket av samma logik du använder idag och välja var du exponerar mutationen som en serveråtgärd eller som en mer traditionell mutation. Du som utvecklare har makten att välja vilka mönster som fungerar bäst för din applikation. Om du inte använder tRPC idag finns det några paket (next-safe-action och zsa kommer att tänka på) som låter dig definiera typsäkra, indatavaliderade serveråtgärder som också är värda att kolla in.
Om du vill se en app som använder detta i praktiken, kolla in Trellix tRPC, en app jag nyligen gjorde som utnyttjar dessa nya primitiv.
Vad tycker du? Vi vill ha din feedback
Så, vad tycker du? Berätta för oss på Github och hjälp oss att iterera för att få dessa primitiv till ett stabilt tillstånd.
Det finns fortfarande arbete kvar, särskilt när det gäller felhantering. Next.js förespråkar att returnera fel, och vi vill göra detta så typsäkert som möjligt. Kolla in denna pågående PR av Alex för tidigt arbete på detta.
Tills nästa gång, lycka till med kodandet!
