tekiehei2317's blog

React Native + tRPCの構成を試してみた

投稿日: 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について

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を使ってみました。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クライアントには、他にaspidazodiosがあります。一応この2つも試してみましたが、zodiosはクライアント側がViteで動かせず、aspidaはコード生成が必要だったので、tRPCが一番使いやすかったです。

npm trendsを見た感じでも、この3つの中ではtRPCが頭ひとつ抜けていました。フルスタックフレームワーク事情にはあまり詳しくないのですが、RemixやNestJSが競合になるのでしょうか。

サーバーサイドTypeScriptが流行ってほしいので、自分も布教していきたいと思います。trpc/serverとPrismaはいいぞ!