😺

怠け者エンジニアがtRPCに入門してみた!

2022/09/27に公開

tRPCとは?

tRPC allows you to easily build & consume fully typesafe APIs, without schemas or code generation.

stRPCを使えばスキーマ設計やらSQL文なんかが無しでも、型セーフティーなAPIをつくれるんだぜ★

若干、意訳が入ってますが、こんなノリかと思います。
正直、何言っちゃんてんの状態。
SQL文を書かなくてもgraphQLのように必要なデータだけとってこれて、それで型まで決まってるので
typeセーフ。
挙句の果てにserver側もTypeScriptでも作れるよってんだから、そんな楽なライブラリあるんかい状態。

きっかけは、RestAPIに疲れ、graphQL学習に頓挫したこと

私は正直、かなりのへなちょこエンジニアです。
恐らく優秀な皆さんなら当てはまらないかもしれないです。

RestAPI使って、型の問題でバックエンド担当者とバチバチになることはしょっちゅうだし、
graphQL最近、かじってアプリ作って本も書いてみたものの、正直、個人開発でここまでやるか(-_-;)
ってのが正直思ってしまった。

https://zenn.dev/takpon1751/books/d630fbfa4e03ac

それと、graphQLサーバーはApolloやらStepZenなんかがあるものの、
結局どれもお金がかかってしまうのが辛いところ。
リリース、運用までできるだけ0円で持っていきたいのが正直なところ。

早速入門!

今回はYouTubeで公開されているTutorialを参考にしてみました。

https://www.youtube.com/watch?v=PKy2lYEnhgs

まだ全ての動画を完了したわけではないですが、公開されているpokemonのAPIを使って
こんな画面を作成していきます。

動画のコードと全く変わりませんが、要点だけを出したい思います。

本当に型セーフなAPI取得が行えていた。

restAPIを呼び出す部分が下記のようなコードです。

src/backend/router/index.ts
import * as trpc from "@trpc/server";
import { z } from "zod";
import { PokemonClient } from "pokenode-ts";

export const appRouter = trpc.router().query("get-pokemon-by-id", {
  input: z.object({ id: z.number() }),
  async resolve({ input }) {
    const api = new PokemonClient();
    const pokemon = await api.getPokemonById(input.id);
    return pokemon;
  },
});

export type AppRouter = typeof appRouter;

このコードではpokemon RestAPIを取得するために、pokenode-tsというライブラリを使用しています。また、入力値やクエリとしてAPI取得時に渡す変数チェックにzodというライブラリも使用しています。zodについては、解説されている方が沢山いらっしゃるので、今回は割愛させていただきます。

また、このフロント側でAPIを呼び出して表示させる部分が下記のようなコードです。

src/pages/index.tsx
import { getOptionsForVote } from "@/utils.ts/getRandomPokemon";
import { trpc } from "@/utils.ts/trpc";
import { useState } from "react";

const Home = () => {
  const [ids, updateIds] = useState(() => getOptionsForVote());

  const [first, second] = ids;

  const firstPokemon = trpc.useQuery(["get-pokemon-by-id", { id: first }]);
  const secondPokemon = trpc.useQuery(["get-pokemon-by-id", { id: second }]);

  if (firstPokemon.isLoading || secondPokemon.isLoading) return null;

  return (
    <div className="h-screen w-screen flex flex-col justify-center items-center">
      <div className="text-2xl text-center">Which Pokemonm is Rounder?</div>
      <div className="p-2" />
      <div className="border rounded p-8 flex justify-between items-center max-w-2xl">
        <div
          suppressHydrationWarning={true}
          className="w-64 h-64 flex flex-col"
        >
          <img
            src={firstPokemon.data?.sprites.front_default}
            className="w-full"
          />
          <div className="text-xl text-center capitalize mt-[-2rem]">
            {firstPokemon.data?.name}
          </div>
        </div>
        <div className="p-8">Vs</div>
        <div
          suppressHydrationWarning={true}
          className="w-64 h-64 flex flex-col"
        >
          <img
            src={secondPokemon.data?.sprites.front_default}
            className="w-full"
          />
          <div className="text-xl text-center capitalize mt-[-2rem]">
            {secondPokemon.data?.name}
          </div>
        </div>
        <div className="p-2"></div>
      </div>
    </div>
  );
};

export default Home;

さて、コードを実行すると下記のように全てのデータが入って来てしまいます。

めっちゃデータ入ってくる、、、こんなに要らんのに(´Д⊂グスン

では、下記の★の部分のように書き換えてみましょう。

src/backend/router/index.ts
import * as trpc from "@trpc/server";
import { z } from "zod";
import { PokemonClient } from "pokenode-ts";

export const appRouter = trpc.router().query("get-pokemon-by-id", {
  input: z.object({ id: z.number() }),
  async resolve({ input }) {
    const api = new PokemonClient();
    const pokemon = await api.getPokemonById(input.id);return { name: pokemon.name, sprites: pokemon.sprites };
  },
});

export type AppRouter = typeof appRouter;

画面側で必要な情報はnameとpokemonの画像データのみなので、★の部分のデータが入ってくるのが理想です。

なな!!必要なデータだけ取れてます。
graphQLのように書かなくても、たったこれだけでした。

しかも、フロント側でも型保管が効くので滅茶便利。

引き続き調査続行!

さて、ここまででテンションが上がりまくてしまいました。
こんな学習コストが低いAPIの定義方法ってありますか?
少なくとも私レベルの個人開発なら十分。
今後もtRPCに関する投稿を頑張りますので、是非皆さんも一度お試しあれ!

Discussion