trpcって知ってますか?
はじめに
みなさんはAPIの定義を何で行なってますか?
Swagger?
GraphQL?
gRPC?
私もGraphQLでアプリとBFFのAPI定義をしたり、gRPCで定義してそれを無理やりRESTAPIの型定義として使っていたりします。
方法はどうであれAPIの型定義はやっぱり欲しいですよね。
いくらTypeScriptとかGoとか型のある言語を使っていても、それをつなぐAPIのインターフェースに型を決めておかないといくらでも事故れますよね…
特にフロントエンドとバックエンドで開発するエンジニアが違うと、こういった定義ファイルがないと思ってたのと違うパラメータが返ってきたり、リクエストボディに入れる型を間違ってしまったりという事故が起きたりすると思います。
なので何かしらの定義ファイルをフロントバック両者で定めておいてこういった事故を起きないようにしているわけです。
定義ファイルがある場合の問題
これは私個人の周りでよく起きていることなのかもしれませんが、定義ファイルを使ってAPIの型定義をしている場合、フロントとバックで参照している定義ファイルのバージョンが違うという問題起きませんか?
フロントは新しい定義ファイルを読み込んで開発しているのに、バックエンドは古いバージョンを読み込んで開発してしまった結果、結合テスト時にエラー地獄になる。(逆もしかり…)みたいなことが起きます。
trpcはそこの問題を少しでも減らすことができそうな気がしています。
trpcってなに?
簡単にまとめると、server側で定義したInterfaceをそのままclient側で取り込んで繋ぎ込みができるという代物です。
公式サイト
対象はBFFのような使用用途でNodejsのサーバがあるようなアーキテクチャをしている場合です。
私の場合は client - (GraphQL) - BFF - (gRPC) - MicroService
となっている環境のGraphQLをtrpcに置き換えることができそうと思って調べてみました。
使ってみる
ここから試しに、client(React)とserver(Experss)で書いていきます。
server
インストールするものは以下
$ yarn add @trpc/server zod
routerを書いていく
import * as trpc from '@trpc/server';
import { z } from 'zod';
import * as trpcExpress from '@trpc/server/adapters/express';
/**
* Context
* headerから値を取得したりなどcontextに詰めたい情報を返す
*/
export const createContext = ({
req,
}: trpcExpress.CreateExpressContextOptions) => {
const getAuthorization = () => {
return req.headers.authorization || '';
};
return {
authorization: getAuthorization(),
};
};
type Context = trpc.inferAsyncReturnType<typeof createContext>;
const createRouter = () => {
return trpc.router<Context>();
};
/**
* Router
* trpcで待ち受けるロジックを書く
* なんとなくGraphQLチックな感じになっている
*/
export const appRouter = createRouter()
.query('hello', {
input: z
.object({
text: z.string().nullish(),
})
.nullish(),
resolve({ input, ctx }) {
return {
greeting: `hello ${input?.text ?? 'world'}`,
authorization: `token is ${ctx.authorization}`,
};
},
})
.mutation('write', {
input: z.object({
text: z.string(),
}),
resolve() {
return {};
},
});
export type AppRouter = typeof appRouter;
ちょっとだけ解説
createRouter
ここで trpc.router
を返しています。これがtrpcのrouterになります。
const createRouter = () => {
return trpc.router<Context>();
};
appRouter
createRouterに生えている query
や mutation
を使って定義していきます。
第一引数に名前、第二引数にrequestとresponseの定義を書いていきます。
input
がrequestで resolve
が実際の処理を書くところで、そこからreturnしているのがresponseになります。
またinputで定義しているobjectに対して nullish()
をつけるとoptionalになります。
export const appRouter = createRouter()
.query('hello', {
input: z
.object({
text: z.string().nullish(),
})
.nullish(),
resolve({ input, ctx }) {
return {
greeting: `hello ${input?.text ?? 'world'}`,
authorization: `token is ${ctx.authorization}`,
};
},
})
.mutation('mutation', {
input: z.object({
text: z.string(),
}),
resolve() {
return {};
},
});
export type AppRouter = typeof appRouter;
client
インストールするものは以下
$ yarn add @trpc/client @trpc/server @trpc/react react-query zod
trpcの基本設定
ここでtrpcのhooksを生成する
import { createReactQueryHooks } from '@trpc/react';
import type { AppRouter } from 'path/to/router';
export const trpc = createReactQueryHooks<AppRouter>();
Providerを定義する
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { trpc } from '@utils/trpc';
function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
trpcが動いているserverのエンドポイント
url: 'http://localhost:3001/trpc',
headers() {
return {
// authorizationをheaderに付与する
authorization: 'Bearer authorization-token',
};
},
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />;
</QueryClientProvider>
</trpc.Provider>
);
}
export default MyApp;
Componentで使う
trpc
に生えている useQuery
や useMutation
を使ってtrpcとつなぐ
第一引数がserver側で待ち受けている名前、第二引数がrequestBodyになります
import type { NextPage } from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { trpc } from '@utils/trpc';
const Home: NextPage = () => {
const hello = trpc.useQuery(['hello', { text: 'John' }]);
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>Welcome to TRPC sample app!!!</h1>
<div className={styles.description}>
{(() => {
if (!hello.data) {
return <div>Loading...</div>;
} else {
return (
<>
<p>{hello.data.greeting}!!!</p>
<p>authorization is {hello.data.authorization}</p>
</>
);
}
})()}
</div>
</main>
</div>
);
};
export default Home;
感想
冒頭でも書いた通りで、schemaとかprotoを挟むことなく、server側で書いたrouterから型を取得する形でclient側を書くことができるので、定義ファイルのことを忘れて開発することができます。
trpcの場合基本的にserver側が正となるので、利用している型情報が違った場合はserver側を参照するといいってなります。
ただserver側の実装がある程度進んでいないといけないので、定義ファイルを固めてclientとserverと同時進行で開発を進めるということができなくなるので、そこはデメリットになるかなーと感じています。
個人的にはtrpcはTypeScriptの知見があればすんなり入れそうなので、GraphQLやgRPCの教育コストが不要になりそうだなというメリットを感じました。
気になる方は一度お試しあれ。
Discussion
tRPCをGraphQLと比較したときのメリットとして、
ってのももちろんあるんだと思いますが、スキーマが不要になることによって得られるメリットは
が大きいようです。
つまるところ、GraphQLは複雑なデータをクライアント側で自在かつ型安全にクエリ/ミューテーションするためのものなのに対して、tRPCは単純に型安全なAPIを簡単に構築するためのものという位置づけですね(ドキュメントのままですが)。
参考