Jump to content

tRPC

O tRPC nos permite escrever APIs seguras de ponta a ponta sem nenhuma geração de cĂłdigo ou sobrecarga de tempo de execução. Ele usa a grande inferĂȘncia do TypeScript para inferir as definiçÔes de tipo do seu roteador de API e permite que vocĂȘ chame seus procedimentos de API de seu front-end com total segurança de tipo e preenchimento automĂĄtico. Ao usar tRPC, seu front-end e back-end parecem mais prĂłximos do que nunca, permitindo uma excelente experiĂȘncia de desenvolvedor.

Criei o tRPC para permitir que as pessoas se movam mais rapidamente, removendo a necessidade de uma camada de API tradicional, enquanto ainda tenho a confiança de que nossos aplicativos não serão interrompidos à medida que iteramos rapidamente.

Avatar of @alexdotjs
Alex - criador do tRPC @alexdotjs

Como eu uso o tRPC?

o contribuidor do tRPC trashh_dev↗ fez uma fala esplĂȘndida na Next.js Conf↗ sobre o tRPC. É altamente recomendĂĄvel que vocĂȘ assista, caso ainda nĂŁo o tenha feito.

Com tRPC, vocĂȘ escreve funçÔes TypeScript em seu back-end e, em seguida, as chama de seu front-end. Um procedimento tRPC simples poderia ser assim:

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

Este é um procedimento tRPC (equivalente a um manipulador de rota em um back-end tradicional) que primeiro valida a entrada usando Zod (que é a mesma biblioteca de validação que usamos para variåveis de ambiente) - neste caso , é garantir que a entrada seja uma string. Se a entrada não for uma string, ela enviarå um erro informativo.

Após a entrada, encadeamos uma função de resolução que pode ser uma consulta↗, mutação↗ ou uma assinatura↗. Em nosso exemplo, o resolvedor chama nosso banco de dados usando nosso cliente prisma e retorna o usuário cujo id corresponde ao que passamos.

VocĂȘ define seus procedimentos em routers que representam uma coleção de procedimentos relacionados com um namespace compartilhado. VocĂȘ pode ter um roteador para users, um para posts e outro para messages. Esses roteadores podem ser mesclados em um Ășnico appRouter centralizado:

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

export type AppRouter = typeof appRouter;

Observe que precisamos apenas exportar as definiçÔes de tipo do nosso roteador, o que significa que nunca importaremos nenhum código de servidor em nosso cliente.

Agora vamos chamar o procedimento em nosso frontend. tRPC fornece um wrapper para o @tanstack/react-query que permite que vocĂȘ utilize todo o poder dos hooks que eles fornecem, mas com o benefĂ­cio adicional de ter suas chamadas de API digitadas e inferidas. Podemos chamar nossos procedimentos de nosso front-end assim:

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

VocĂȘ notarĂĄ imediatamente como o preenchimento automĂĄtico e a segurança de tipo sĂŁo bons. Assim que vocĂȘ escrever api., seus roteadores aparecerĂŁo no preenchimento automĂĄtico, e quando vocĂȘ selecionar um roteador, seus procedimentos tambĂ©m aparecerĂŁo. VocĂȘ tambĂ©m receberĂĄ um erro de TypeScript se sua entrada nĂŁo corresponder ao validador definido no back-end.

Inferindo erros

Por padrĂŁo, create-t3-app configura um formatador de erros↗ que permite que vocĂȘ infira os erros do Zod se vocĂȘ receber erros de validação no back-end.

Exemplo de uso:

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` retornado com um erro no `title` */}
        <span className="mb-8 text-red-500">
          {error.data.zodError.fieldErrors.title}
        </span>
      )}

      ...
    </form>
  );
}

Arquivos

O tRPC requer bastante do template que o create-t3-app configura para vocĂȘ. Vamos ver os arquivos que sĂŁo gerados:

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

Este Ă© o ponto de entrada para sua API que expĂ”e o roteador tRPC. Normalmente, vocĂȘ nĂŁo mexerĂĄ muito nesse arquivo, mas se precisar, por exemplo, habilitar o middleware CORS ou similar, Ă© Ăștil saber que o createNextApiHandler exportado Ă© um handler da API do Next.js↗ que recebe uma request↗ e response↗. Isso significa que vocĂȘ pode agrupar o createNextApiHandler em qualquer middleware que desejar. Veja abaixo um trecho de exemplo da adição de CORS.

📄 server/api/trpc.ts

Este arquivo é dividido em duas partes, criação de contexto e inicialização do tRPC:

  1. Definimos o contexto que é passado para seus procedimentos tRPC. O contexto são dados aos quais todos os seus procedimentos tRPC terão acesso, e é um ótimo lugar para colocar coisas como conexÔes com banco de dados, informaçÔes de autenticação, etc. Em create-t3-app, usamos duas funçÔes, para habilitar o uso de um subconjunto do contexto quando não temos acesso ao objeto de solicitação.
  • createInnerTRPCContext: É aqui que vocĂȘ define o contexto que nĂŁo depende da solicitação, por exemplo sua conexĂŁo com o banco de dados. VocĂȘ pode usar esta função para teste de integração ou ssg-helpers↗ onde vocĂȘ nĂŁo tem um objeto de solicitação/request.

  • createTRPCContext: É aqui que vocĂȘ define o contexto que depende da solicitação, por exemplo a sessĂŁo do usuĂĄrio. VocĂȘ solicita a sessĂŁo usando o objeto opts.req e, em seguida, passa a sessĂŁo para a função createContextInner para criar o contexto final.

  1. Inicializamos o tRPC e definimos procedures↗ e middlewares↗ reutilizĂĄveis. Por convenção, vocĂȘ nĂŁo deve exportar o objeto inteiro t, mas sim criar procedures e middlewares reutilizĂĄveis e exportĂĄ-los.

VocĂȘ perceberĂĄ que usamos ‘superjson’ como transformador de dados↗. Isso faz com que seus tipos de dados sejam preservados quando eles chegam ao cliente, entĂŁo, por exemplo, se vocĂȘ enviar um objeto Date, o cliente retornarĂĄ um Date e nĂŁo uma string, que Ă© o caso para a maioria das APIs.

📄 server/api/routers/*.ts

Aqui Ă© onde vocĂȘ define as rotas e procedimentos da sua API. Por convenção, vocĂȘ cria rotas separados↗ para procedimentos relacionados.

📄 server/api/root.ts

Aqui mesclamos↗ todos as sub-rotas definidas em routers/** em um Ășnico roteador de aplicativo.

📄 utils/api.ts

Este Ă© o ponto de entrada do front-end para tRPC. É aqui que vocĂȘ importarĂĄ a definição de tipo do roteador e criarĂĄ seu cliente tRPC junto com os hooks do react-query. Como habilitamos superjson como nosso transformador de dados no back-end, precisamos habilitĂĄ-lo tambĂ©m no front-end. Isso ocorre porque os dados serializados do back-end sĂŁo “desserializados” no front-end.

VocĂȘ definirĂĄ seus links tRPC↗ aqui, que determinarĂŁo o fluxo de solicitação do cliente para o servidor. Usamos o “padrĂŁo” httpBatchLink↗ que permite solicitar lotes↗, bem como um loggerLink↗ que gera logs de solicitação Ășteis durante o desenvolvimento.

Por fim, exportamos um tipo auxiliar↗ que vocĂȘ pode usar para inferir seus tipos no frontend.

Contribuidor do Create T3 App Christopher Ehrlich↗ fez um vĂ­deo sobre fluxos de dados em tRPC↗. Este vĂ­deo Ă© recomendado se vocĂȘ jĂĄ usou o tRPC, mas ainda se sente um pouco incerto sobre como ele funciona.

Como faço para chamar minha API externamente?

Com APIs regulares, vocĂȘ pode chamar seus endpoints usando qualquer cliente HTTP, como curl, Postman, fetch, Insomnia ou diretamente do seu navegador. Com tRPC, Ă© um pouco diferente. Se vocĂȘ deseja chamar seus procedimentos sem o cliente tRPC, hĂĄ duas maneiras recomendadas de fazer isso:

Expor um Ășnico procedimento externamente

Se vocĂȘ deseja expor um Ășnico procedimento externamente, estĂĄ procurando por chamadas do lado do servidor↗. Isso permitiria que vocĂȘ criasse um terminal de API Next.js normal, mas reutilizasse a parte do resolvedor de seu procedimento tRPC.

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;

Expondo cada procedimento como um endpoint REST

Se vocĂȘ deseja expor todos os procedimentos externamente, verifique o plug-in criado pela comunidade trpc-openapi↗. Ao fornecer alguns metadados extras para seus procedimentos, vocĂȘ pode gerar uma API REST compatĂ­vel com OpenAPI a partir de seu roteador tRPC.

SĂŁo apenas Requests HTTP

O tRPC se comunica por meio de HTTP, portanto, tambĂ©m Ă© possĂ­vel chamar seus procedimentos tRPC usando solicitaçÔes HTTP “regulares”. No entanto, a sintaxe pode ser complicada devido ao protocolo RPC↗ que o tRPC usa. Se vocĂȘ estiver curioso, pode verificar como sĂŁo as solicitaçÔes e respostas tRPC na guia de rede do seu navegador, mas sugerimos fazer isso apenas como um exercĂ­cio educacional e aderir a uma das soluçÔes descritas acima.

Comparação com um endpoint da API Next.js

Vamos comparar um endpoint da API Next.js com um procedimento tRPC. Digamos que queremos buscar um objeto de usuĂĄrio de nosso banco de dados e retornĂĄ-lo ao frontend. PoderĂ­amos escrever uma rota de API Next.js como esta:

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

Compare isso com o exemplo tRPC acima e vocĂȘ verĂĄ algumas das vantagens do tRPC:

  • Em vez de especificar um URL para cada rota, o que pode ser irritante de depurar se vocĂȘ mover algo, todo o seu roteador Ă© um objeto com preenchimento automĂĄtico.
  • VocĂȘ nĂŁo precisa validar qual mĂ©todo HTTP foi usado.
  • VocĂȘ nĂŁo precisa validar se a consulta ou o corpo da solicitação contĂ©m os dados corretos no procedimento, pois o Zod cuida disso.
  • Em vez de criar uma resposta, vocĂȘ pode lançar erros e retornar um valor ou objeto como faria em qualquer outra função TypeScript.
  • Chamar o procedimento no frontend fornece preenchimento automĂĄtico e segurança de tipo.

Snippets Ășteis

Aqui estĂŁo alguns snippets que podem ser Ășteis.

Ativando o CORS

Se vocĂȘ precisar consumir sua API de um domĂ­nio diferente, por exemplo, em um monorepo que inclua um aplicativo React Native, talvez seja necessĂĄrio habilitar o 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) => {
  // Ativar CORS
  await cors(req, res);

  // Criar e chamar o handler do tRPC
  return createNextApiHandler({
    router: appRouter,
    createContext: createTRPCContext,
  })(req, res);
};

export default handler;

AtualizaçÔes otimistas

As atualizaçÔes otimistas ocorrem quando atualizamos a interface do usuĂĄrio antes que a chamada da API seja concluĂ­da. Isso dĂĄ ao usuĂĄrio uma experiĂȘncia melhor porque ele nĂŁo precisa esperar que a chamada da API termine antes que a interface do usuĂĄrio reflita o resultado de sua ação. No entanto, aplicativos que valorizam muito a exatidĂŁo dos dados devem evitar atualizaçÔes otimistas, pois nĂŁo sĂŁo uma representação “verdadeira” do estado de back-end. VocĂȘ pode ler mais na documentação do React Query↗.

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

  const utils = api.useContext();
  const postCreate = api.post.create.useMutation({
    async onMutate(newPost) {
      // Cancele as requisiçÔes de saída (para que não substituam nossa atualização otimista)
      await utils.post.list.cancel();

      // Obtenha os dados do queryCache
      const prevData = utils.post.list.getData();

      // Atualizar os dados de forma otimista com nosso novo post
      utils.post.list.setData(undefined, (old) => [...old, newPost]);

      // Retornar os dados anteriores para que possamos reverter se algo der errado
      return { prevData };
    },
    onError(err, newPost, ctx) {
      // Se a mutation falhar, usar o valor de contexto de onMutate
      utils.post.list.setData(undefined, ctx.prevData);
    },
    onSettled() {
      // Sincronizar com o servidor assim que a mutação for estabelecida
      utils.post.list.invalidate();
    },
  });
};

Exemplo de teste de integração

Aqui está um exemplo de teste de integração que usa Vitest↗ para verificar se seu roteador tRPC está funcionando conforme o esperado, se o analisador de entrada infere o tipo correto e se os dados retornados correspondem à saída esperada.

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

Se seu procedimento estiver protegido, vocĂȘ pode passar um objeto session mockado quando criar o contexto:

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

  // ...
});

Recursos Úteis

RecursoLink
Documentação do tRPChttps://www.trpc.io↗
Muitos exemplos de tRPChttps://github.com/trpc/trpc/tree/next/examples↗
Documentação do React Queryhttps://tanstack.com/query/v4/docs/adapters/react-query↗