Jump to content

Attention: This page is 14 days older than the English version and might be outdated. If you're a native speaker of this language and would like to contribute to the project, please consider updating this page to match the latest English version.

You can also view the English version of this page.

tRPC

tRPC pozwala nam pisanie API b─Öd─ůcych w pe┼éni typesafe bez ┼╝adnego generowania kodu czy te┼╝ za┼Ťmiecania runtimeÔÇÖu. Korzysta on ze ┼Ťwietnego type inference od Typecripta aby przekazywa─ç definicje router├│w oraz pozwala Ci na korzystanie z procedur API na frontendzie z pe┼énym tyepsafety i autouzupe┼énianiem. Je┼Ťli korzystasz z tRPC, tw├│j front- i backend b─Öd─ů sprawia┼éy wra┼╝enie bycia bardziej po┼é─ůczonymi ni┼╝ kiedykolwiek, pozwalaj─ůc na niespotykany DX (developer experience).

Zbudowa┼éem tRPC aby umo┼╝liwi─ç ka┼╝demu szybsze robienie post─Öp├│w, usuwaj─ůc przy tym potrzeb─Ö korzystania z tradycyjnej wartswy API oraz zachowuj─ůc pewno┼Ť─ç, i┼╝ nasze aplikacje nie zepsuj─ů si─Ö nad─ů┼╝aj─ůc za w┼éasnym rozwojem.
Oryginał: I built tRPC to allow people to move faster by removing the need of a traditional API-layer, while still having confidence that our apps won't break as we rapidly iterate.

Avatar of @alexdotjs
Alex - tw├│rca tRPC @alexdotjs

Pliki

tRPC wymaga du┼╝o boilerplateÔÇÖu, kt├│ry create-t3-app przygotowuje za Ciebie. Przejd┼║my wi─Öc po kolei po plikach, kt├│re s─ů generowane:

­čôä pages/api/trpc/[trpc].ts

Jest to w┼éa┼Ťciwy punkt pocz─ůtkowy dla twojego API - to on ujawnia dla reszty aplikacji tw├│j router od tRPC. Prawdopodobnie nie b─Ödziesz musia┼é edytowa─ç tego pliku, ale je┼╝eli zajdzie taka potrzeba (np. do w┼é─ůczenia CORSa), warto wiedzie─ç o tym, i┼╝ eksportowany createNextApiHandler to Next.js API handlerÔćŚ, kt├│ry pobiera obiekt zapytaniaÔćŚ i odpowiedziÔćŚ serwera. Oznacza to, i┼╝ mo┼╝esz zawrze─ç createNextApiHandler w middlewareÔÇÖrze, w jakim tylko chcesz. Poni┼╝ej znajdziesz przyk┼éadowy kod, dzi─Öki kt├│remu dodasz CORS.

­čôä server/api/trpc.ts

Plik ten podzielony jest na dwie cz─Ö┼Ťci - tworzenie kontekstu oraz inicjalizacji tRPC:

  1. Definiujemy kontekst przesy┼éany do procedur tRPC. Kontekt, to dane do kt├│rych dost─Öp maj─ů wszystkie twoje procedury tRPC. Jest to doskona┼ée miejsce do umieszczenia rzeczy, takich jak po┼é─ůczenia z baz─ů danych, informacje o uwierzytelnianiu, itp. W Create T3 App korzystamy z dw├│ch funkcji, aby umo┼╝liwi─ç korzystanie z cz─Ö┼Ťci kontekstu bez dost─Öpu do obiektu zapytania.
  • createInnerTRPCContext: Tutaj definiujesz kontekst, kt├│ry nie zale┼╝y od obiektu zapytania, np. po┼é─ůczenie z baz─ů danych. Mo┼╝esz wykorzysta─ç funkcj─Ö t─ů do test├│w integracji oraz funkcji pomocniczych SSGÔćŚ, gdzie nie posiadasz obiektu zapytania.

  • createTRPCContext: Tutaj definiujesz kontekst, kt├│ry zale┼╝ny jest od zapytania, np. sesja u┼╝ytkownika. Otrzymujesz sesj─Ö korzystaj─ůc z obiektu opts.req a nast─Öpnie posy┼éasz j─ů do funkcji createInnerTRPCContext w celu utworzenia finalnego kontekstu.

  1. Inicjalizujemy tRPC i definiujemy proceduryÔćŚ oraz middlewareÔÇÖyÔćŚ. Umownie, nie powiniene┼Ť eksportowa─ç ca┼éego obiektu t a jedynie poszczeg├│lne procedury i middlewareÔÇÖy.

Zwr├│─ç uwag─Ö, i┼╝ korzystamy z paczki superjson jako transformera danychÔćŚ. Umo┼╝liwia on na zachowanie typ├│w danych, kt├│re otrzymuje klient - przyk┼éadowo, posy┼éaj─ůc obiekt Date, klient r├│wnie┼╝ otrzyma obiekt Date - a nie tekst, w przeciwie┼ästwie do wielu innych API.

­čôä server/api/routers/*.ts

Tutaj definiujesz routery i procedury swojego API. Umownie, powiniene┼Ť tworzy─ç osobne routeryÔćŚ dla odpowiadaj─ůcych im procedur.

­čôä server/api/root.ts

Tutaj ┼é─ůczymyÔćŚ wszystkie ÔÇťsub-routeryÔÇŁ zdefiniowane w folderze routers/** w jeden router aplikacji.

­čôä utils/api.ts

Jest to punkt startowy tRPC po stronie frontendu. To tutaj importowa─ç b─Ödziesz wszystkie definicje typ├│w i tworzy─ç b─Ödziesz sw├│j client tRPC razem z hookami od react-query. Poniewa┼╝ korzystamy z paczki superjson jako transformera danych na backendzie, musimy go uruchomi─ç r├│wnie┼╝ na frontendzie. Dzieje si─Ö tak, poniewa┼╝ dane serializowane w API musz─ů by─ç dekodowane w┼éa┼Ťnie na frontendzie.

Zdefiniujesz tu tak┼╝e linkiÔćŚ tRPC, kt├│re decyduj─ů o ca┼éym flow zapytania - od klienta do serwera. My korzystamy z ÔÇťdomy┼ŤlnegoÔÇŁ linku httpBatchLinkÔćŚ, kt├│ry umo┼╝liwia ÔÇťrequest batchingÔÇŁÔćŚ. Korzystamy te┼╝ z linku loggerLinkÔćŚ, pozwalaj─ůcego na wy┼Ťwietlanie przydatnych podczas pisania aplikacji log├│w.

Na koniec eksportujemy pomocniczy typÔćŚ, kt├│rego u┼╝y─ç mo┼╝esz do dziedziczenia typ├│w na frontendzie.

Jak korzysta─ç z tRPC?

Kontrybutor tRPC trashh_devÔćŚ zrobi┼é znakomity wyst─Öp na Next.js ConfÔćŚ w┼éa┼Ťnie o tRPC. Je┼╝eli jeszcze si─Ö z nim nie zapozna┼ée┼Ť, bardzo polecamy Ci to zrobi─ç.

Z tRPC, piszesz funkcje w TypeScriptÔÇÖcie na backendzie a nast─Öpnie wywo┼éujesz je z frontendu. Prosta procedura tRPC wygl─ůda─ç mo┼╝e tak:

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

Jest to procedura (odpowiednik handlera routeÔÇÖa w tradycyjnym API), kt├│ra najpierw waliduje wej┼Ťcie/input korzystaj─ůc z biblioteki Zod (jest to ta sama biblioteka, z kt├│rej korzystamy podczas sprawdzania zmiennych ┼Ťrodowiskowych) - w tym przypadku zapewnia ona, i┼╝ dane przes┼éane do API s─ů w formie tekstu (stringa). Je┼╝eli jednak nie jest to prawda, API wy┼Ťle informatywny b┼é─ůd.

Po sprawdzeniu wej┼Ťcia, do┼é─ůczamy funkcj─Ö, kt├│ra mo┼╝e by─ç albo queryÔćŚ, albo mutacj─ůÔćŚ, albo subscrypcj─ůÔćŚ. W naszym przyk┼éadzie, funkcja ta (zwana ÔÇťresolveremÔÇŁ) wysy┼éa zapytanie do bazy danych korzystaj─ůc z naszego klienta prisma i zwraca u┼╝ytkownika z pasuj─ůcym do wys┼éanego id.

Swoje procedury definiujesz w folderze routers, kt├│ry reprezentuje kolekcj─Ö pasuj─ůcych procedur ze wsp├│lnej przestrzeni. Mo┼╝esz mie─ç router users, router posts i router messages. Routery te mog─ů zosta─ç nast─Öpnie po┼é─ůczone w jeden, scentralizowany appRouter:

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

export type AppRouter = typeof appRouter;

Zwr├│─ç uwag─Ö na to, i┼╝ musimy eksportowa─ç jedynie definicje typ├│w tego routera - oznacza to, i┼╝ nigdy nie importujemy kodu serwera po stronie klienta.

Wywo┼éajmy teraz procedur─Ö na naszym frontendzie. tRPC dostarcza nam wrapper dla paczki @tanstack/react-query, kt├│ry pozwala ci wykorzysta─ç pe┼én─ů moc hook├│w. Dodatkowo, zapytania API dostajesz w pe┼éni ÔÇťotypowaneÔÇŁ. Zapytanie do naszych procedur mo┼╝emy wykona─ç w nast─Öpuj─ůcy spos├│b:

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

Natychmiast zauwa┼╝ysz, jak dobrze dzia┼éa type-safety i autouzupe┼énianie. Jak tylko napiszesz trpc., twoje routery automatycznie pojawi─ů si─Ö w opcjach autopodpowiedzi a kiedy tylko wybierzesz router, r├│wnie┼╝ znajd─ů si─Ö tam jego procedury. Otrzymasz tak┼╝e b┼é─ůd TypeScripta, je┼╝eli wej┼Ťcie (input) nie b─Ödzie zgadza─ç si─Ö z tym, podanym do systemu walidacji na backendzie.

Jak wykona─ç zewn─Ötrzne zapytania do mojego API?

Korzystaj─ůc z regularnego API, zapytania takie mo┼╝esz wykona─ç korzystaj─ůc z klient├│w HTTP takich jak curl, Postman, fetch, czy tez bezpo┼Ťrednio z przegl─ůdarki. Z tRPC sprawa wygl─ůda jednak inaczej. Je┼╝eli chcesz wykona─ç takie zapytania bez klienta tRPC, mo┼╝esz skorzysta─ç z jedngo z dw├│ch polecanych na to sposob├│w:

Ujawnianie zewn─Ötrznie pojedynczej procedury tRPC

Je┼╝eli chcesz ujawni─ç zewn─Ötrznie pojedyncz─ů procedur─Ö, powiniene┼Ť skorzysta─ç z zapyta┼ä po stronie serweraÔćŚ. Pozwoli Ci to na wykonanie standardowego endpointa Next.js, ale u┼╝yje cz─Ö┼Ťci ÔÇťresolveraÔÇŁ twojej procedury tRPC.

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

const userByIdHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  // Stwórz kontekst i obiekt zapytań
  const ctx = await createTRPCContext({ req, res });
  const caller = appRouter.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) {
      // Wyst─ůpi┼é b┼é─ůd z tRPC
      const httpCode = getHTTPStatusCodeFromError(cause);
      return res.status(httpCode).json(cause);
    }
    // Wyst─ůpi┼é inny b┼é─ůd
    console.error(cause);
    res.status(500).json({ message: "Internal server error" });
  }
};

export default userByIdHandler;

Ujawnianie wszystkich procedur tRPC jako endpoint├│w REST

Je┼╝eli chcesz ujawni─ç zewn─Ötrznie wszystkie procedury tRPC, sprawd┼║ rozszerzenie stworzone przez spo┼éeczno┼Ť─ç - trpc-openapiÔćŚ. Dostarczaj─ůc dodatkowych metadanych do twoich procedur, wygenerowa─ç mo┼╝esz REST API zgodne z OpenAPI ze swoich router├│w tRPC.

To tylko zapytania HTTP

tRPC komunikuje si─Ö za pomoc─ů HTTP, wi─Öc masz tak┼╝e mo┼╝liwo┼Ť─ç wykonywania zapyta┼ä do swoich procedur korzystaj─ůc w┼éa┼Ťnie z ÔÇťregularnychÔÇŁ zapyta┼ä HTTP. Sk┼éadnia mo┼╝e wydawa─ç si─Ö jednak niepor─Öczna z powodu wykorzystywanego przez tRPC protoko┼éu RPCÔćŚ. Je┼╝eli jeste┼Ť ciekawy jak on dzia┼éa, mo┼╝esz zobaczy─ç jak wygl─ůdaj─ů zapytania tRPC w zak┼éadce ÔÇťsie─çÔÇŁ w swojej przegl─ůdarce - polecamy robi─ç to jednak tylko w celach edukacyjnych i skorzysta─ç z jednego z rozwi─ůza┼ä przedstawionych powy┼╝ej.

Por├│wnanie do endpointu API Next.js

Por├│wnajmy endpoint API Next.js z procedur─ů tRPC. Powiedzmy, ┼╝e chcemy pobra─ç ubiekt u┼╝ytkownika z naszej bazy danych i zwr├│ci─ç go na frontend. Endpoint API Next.js napisa─ç mogliby┼Ťmy w nast─Öpuj─ůcy spos├│b>+:

pages/api/users/[id].ts
import type { NextApiRequest, 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]);
};

Por├│wnaj to do powy┼╝szego przyk┼éadu z tRPC - zobaczysz zalety korzystanie w┼éa┼Ťnie z tego sposobu:

  • Zamiast precyzowa─ç url dla ka┼╝dego routeÔÇÖa (co mo┼╝e sta─ç si─Ö uci─ů┼╝liwe do debugowania, je┼Ťli co┼Ť przeniesiesz), tw├│j ca┼éy router jest obiektem z autouzupe┼énianiem.
  • Nie musisz walidowa─ç u┼╝ytej metody HTTP.
  • Nie musisz walidowa─ç zawarto┼Ťci zapytania pod k─ůtem pooprawno┼Ťci zawartych danych - zajmuje si─Ö tym Zod.
  • Zamiast tworzy─ç obiekt ÔÇťresponseÔÇŁ, mo┼╝esz wyrzuca─ç b┼é─Ödy i zwraca─ç warto┼Ťci lub obiekty tak, jak robi┼éby┼Ť to w zwyk┼éej funkcji TypeScripta.
  • Wywo┼éywanie procedury na frontendzie dostarcza Ci autouzupe┼éniania i type-safety.

Przydatne fragmenty

Znajdziesz tutaj fragmenty kodu, kt├│re mog─ů Ci si─Ö przyda─ç.

Aktywacja CORS

Je┼╝eli chcesz korzysta─ç z API z r├│┼╝nych domen, np. w monorepo zawieraj─ůcym aplikacj─Ö React Native, mo┼╝esz chcie─ç w┼é─ůczy─ç CORS:

pages/api/trpc/[trpc].ts
import type { NextApiRequest, 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) => {
  // W┼é─ůcz cors
  await cors(req, res);

  // Stwórz i wywołaj handler tRPC
  return createNextApiHandler({
    router: appRouter,
    createContext: createTRPCContext,
  })(req, res);
};

export default handler;

ÔÇťOptimistic updatesÔÇŁ

Aktualizacje danych zwane ÔÇťOptimistic updatesÔÇŁ zachodz─ů wtedy, kiedy aktualizujemy UI, zanim zapytanie API zostanie uko┼äczone. Dostarcza to lepsze do┼Ťwiadczenie u┼╝ytkownika, poniewa┼╝ nie musi on czeka─ç na uko┼äczenie zapytania API, aby zobaczy─ç odzwierciedlenie zmian w interfejsie aplikacji. Pami─Ötaj jednak, ┼╝e aplikacje, kt├│re ceni─ů sobie poprawno┼Ť─ç danych, powinny za wszelk─ů cen─Ö unika─ç aktualizacji ÔÇťoptimisic updatesÔÇŁ - nie s─ů one ÔÇťpoprawn─ůÔÇŁ reprezentacj─ů stanu backendu. Wi─Öcej na ich temat mo┼╝esz poczyta─ç w dokumentacji React QueryÔćŚ.

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

  const utils = api.useContext();
  const postCreate = api.post.create.useMutation({
    async onMutate(newPost) {
      // Anuluj wychodz─ůce zapytania (aby nie nadpisa┼éy one "optimistic update'u")
      await utils.post.list.cancel();

      // Otrzymaj dane z queryCache
      const prevData = utils.post.list.getData();

      // Zaktualizuj dane z naszego nowego postu
      utils.post.list.setData(undefined, (old) => [...old, newPost]);

      // Zwróć poprzednie dane, aby w razie błędu można było z nich przywrócić stan aplikacji
      return { prevData };
    },
    onError(err, newPost, ctx) {
      // Je┼╝eli mutacja wyrzuci b┼é─ůd, skorzystaj z warto┼Ťci kontekstu z onMutate
      utils.post.list.setData(undefined, ctx.prevData);
    },
    onSettled() {
      // Zsynchronizuj z serwerem po ukończonej mutacji
      utils.post.list.invalidate();
    },
  });
};

Przykładowy Test Integracji

Tu znajdziesz przyk┼éadowy test integracji korzystaj─ůcy z paczki VitestÔćŚ, aby sprawdzi─ç, czy router tRPC dzia┼éa poprawnie, czy parser danych wej┼Ťciowych dziedziczy odpowiedni typ, oraz czy zwracane dane pasuj─ů do oczekiwanego outputu.

import { type inferProcedureInput } from "@trpc/server";
import { createInnerTRPCContext } from "~/server/api/trpc";
import { appRouter, type AppRouter } from "~/server/api/root";
import { expect, test } from "vitest";

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

Przydatne Zasoby

Zas├│bLink
Dokumentacja tRPChttps://www.trpc.ioÔćŚ
Par─Ö przyk┼éad├│w z tRPChttps://github.com/trpc/trpc/tree/next/examplesÔćŚ
Dokumentacja React Queryhttps://tanstack.com/query/v4/docs/adapters/react-queryÔćŚ

Recent Contributors To This Page