投稿日: 2022-10-23
仕事でモバイル+Web管理画面+バックエンドを一人で作ることになりました。弊社はバックエンドはLaravelで作ることが多いのですが、全部TypeScriptで書きたかったので、最近話題になっていたtRPCを使った構成を試してみました。
tRPC - Move Fast and Break Nothing. End-to-end typesafe APIs made easy.
作ったアプリはこちら。メモの登録ができる簡易的なアプリです。
tekihei2317/react-native-memo-app
使ったライブラリはReact Native(Expo)、tRPC、Express、Prismaです。tRPCのサーバーのアダプタは、ExpressとFastifyから選べます。
tRPCは型安全なAPIを作るためのライブラリです。サーバー側とクライアント側を両方TypeScriptで実装する必要があります。
tRPCでは、サーバー側でtRPCのルーターを実装し、その型使ってAPIクライアントを初期化します。こうすることで、APIの呼び出し部分を、型に守られた関数呼び出しとして書けます。
具体的には、tRPCのルーターは以下のように定義します。この例では、メモの一覧の取得処理と登録処理をもったmemoRouter
を作成し、それをおおもとのappRouter
に登録しています。
// backend/src/trpc/router/_memo.ts
import { z } from 'zod'
import { router, publicProcedure } from '../trpc'
const createMemoSchema = z.object({
text: z.string().max(50),
})
const getMemosProcedure = publicProcedure.query(async ({ ctx }) => {
return await ctx.prisma.memo.findMany()
})
const createMemoProcedure = publicProcedure.input(createMemoSchema).mutation(async ({ ctx, input }) => {
const memo = await ctx.prisma.memo.create({
data: input,
})
return memo
})
export const memoRouter = router({
getMemos: getMemosProcedure,
createMemo: createMemoProcedure,
})
// backend/src/trpc/router/_app.ts
import { router } from '../trpc'
import { memoRouter } from './memo'
export const appRouter = router({
memo: memoRouter,
})
export type AppRouter = typeof appRouter
クライアント側の実装は以下です。tRPCクライアントのAPI呼び出しは、ルーターに登録した関数をそのまま呼び出す形になっています。これらの引数や戻り値の型は、サーバー側で定義したものになります。
// mobile-app/utils/trpc.ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import { AppRouter } from '../../backend/src/trpc/router/_app'
import superjson from 'superjson'
export const trpc = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
httpBatchLink({
url: 'http://192.168.1.6:4000/trpc',
}),
],
})
// 使用側
await trpc.memo.createMemo.mutate({ text })
await trpc.memo.getMemos.query()
React Queryをラップしたクライアントがあるので、実際の開発ではそれを使うことになると思います。
ワークスペースの管理はpnpmを使ってみました。pnpmを使った理由は、npm workspacesとは違ってnode_modulesが各パッケージの中に作られるからです。これは、各パッケージからnpmスクリプトを実行しやすいというメリットがあります(npmスクリプトを実行するときは、./node_modules/.binにパスが通るため)。
しかし、Expoを使う場合はnode_modulesはプロジェクトルートにある必要があり、.npmrcでこの挙動を変える必要がありました。
Working with Monorepos - Expo Documentation
こうなるとpnpmのメリットが薄れるので、次はいつも通りnpmかyarnを使おうと思います。node_modulesがカレントディレクトリ配下にないときのnpmスクリプトの実行については、今度調べます。
上記の実装では、サーバー側で定義した型をモバイル側で直接インポートしています。Webも作ることを考えるとちょっと治安が悪くなりそうなので、以下のようにモジュールを分割しようと思っています。
.
├── memo-backend/ tRPCサーバーの実装。api-clientからのみ参照可能。
├── memo-mobile/
├── memo-web/
└── packages/
└── api-client/ tRPCクライアント。memo-mobileまたはmemo-webからのみ参照可能。
api-clientでは、Webやモバイルで使用するもののみ公開します。例えば、tRPCクライアントの初期化に必要なルーターの型や、プロシージャーの入力の型などです。
実際に試してみてから、良かったところや課題を書きたいと思います。
型安全なAPIクライアントには、他にaspidaやzodiosがあります。一応この2つも試してみましたが、zodiosはクライアント側がViteで動かせず、aspidaはコード生成が必要だったので、tRPCが一番使いやすかったです。
npm trendsを見た感じでも、この3つの中ではtRPCが頭ひとつ抜けていました。フルスタックフレームワーク事情にはあまり詳しくないのですが、RemixやNestJSが競合になるのでしょうか。
サーバーサイドTypeScriptが流行ってほしいので、自分も布教していきたいと思います。trpc/serverとPrismaはいいぞ!