Jump to content

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 frontend 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

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.

Korzystanie z błędów biblioteki Zod

Domy┼Ťlnie create-t3-app konfiguruje error formatterÔćŚ, kt├│ry pozwala pobiera─ç b┼é─Ödy z biblioteki Zod, je┼Ťli na backendzie wyst─ůpi─ů b┼é─Ödy walidacji.

Przykładowe użycie:

function MyComponent() {
  const { mutate, error } = api.post.create.useMutation();

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const formData = new FormData(e.currentTarget);
      mutate({ title: formData.get('title') });
    }}>
      <input name="title" />
      {error?.data?.zodError?.fieldErrors.title && (
        {/** `mutate` returned with an error on the `title` */}
        <span className="mb-8 text-red-500">
          {error.data.zodError.fieldErrors.title}
        </span>
      )}

      ...
    </form>
  );
}

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, 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. Kontekst, 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─ç t─ů funkcj─Ö 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ÔćŚ. Umownie, nie powiniene┼Ť eksportowa─ç ca┼éego obiektu t a jedynie poszczeg├│lne procedury i middleware.

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

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, 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) => {
  // 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 { 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" });
});

Je┼╝eli twoja procedura jest chroniona, mo┼╝esz przes┼éa─ç stworzony obiekt session tworz─ůc kontekst:

test("protected example router", async () => {
  const ctx = await createInnerTRPCContext({
    session: {
      user: { id: "123", name: "John Doe" },
      expires: "1",
    },
  });
  const caller = appRouter.createCaller(ctx);
  // ...
});

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