SvelteKitでtRPCを使う

## はじめに
TypeScript界隈で[tRPC](https://trpc.io/)が採用されるケースが増えている感覚がありますが、今回は[SvelteKit](https://kit.svelte.jp/)でtRPCを使う方法について解説しようと思います。SvelteKitとtRPCを組み合わせることで、フルスタックのTypeScriptアプリケーションを簡単に作成できます。tRPCは、クライアントとサーバー間の通信を型安全に行うことができる素晴らしいツールです。そして、[trpc-sveltekit](https://github.com/icflorescu/trpc-sveltekit)を使うことで、SvelteKitとtRPCの統合がとても簡単になります。

## tRPCとは
tRPCは、TypeScriptベースのRPC（Remote Procedure Call）フレームワークです。tRPCを使うと、APIの定義とクライアントとの通信を型安全に行うことができます。これにより、エンドポイントごとに異なる型を定義しなくても、TypeScriptの型推論を最大限に活用できるようになります。tRPCの主な特徴は以下の通りです。

- 型安全: サーバーとクライアント間の通信が完全に型安全。
- シンプル: 簡単にセットアップして使用できる。
- 柔軟: RESTやGraphQLのような特定のプロトコルに縛られない。

## trpc-sveltekitとは
trpc-sveltekitは、SvelteKitプロジェクトでtRPCを簡単に統合できるパッケージです。このパッケージを使うことで、SvelteKitの強力な機能とtRPCの型安全な通信を組み合わせることができます。trpc-sveltekitは、SvelteKitのエンドポイントとtRPCのルーターをシームレスに統合し、開発者の負担を軽減します。

## 導入方法
SvelteKitのプロジェクトがすでにセットアップ済みである前提で、必要なパッケージのインストールをしてゆきます。
```bash
npm install @trpc/server @trpc/client trpc-sveltekit
```

### tRPCルーターの設定
src/lib/trpc/router.tsを作成し、以下のコードを追加します。
```ts
import type { Context } from '$lib/trpc/context';
import { initTRPC } from '@trpc/server';
import delay from 'delay';

export const t = initTRPC.context<Context>().create();

export const router = t.router({
  greeting: t.procedure.query(async () => {
    await delay(500); // 👈 500msかかる処理をシミュレーション
    return `Hello tRPC @ ${new Date().toLocaleTimeString()}`;
  })
});

export type Router = typeof router;
```

### tRPC Contextを作成
src/lib/trpc/context.tsを作成し、以下のコードを追加します。
```ts
import type { RequestEvent } from '@sveltejs/kit';
import type { inferAsyncReturnType } from '@trpc/server';

export async function createContext(event: RequestEvent) {
  return {
    // eventからlocalsやurlなどのオブジェクトが取得できます
  };
}

export type Context = inferAsyncReturnType<typeof createContext>;
```

### handleにtRPCHandleを追加
src/hooks.server.tsのhandleに以下のコードを追加します。
```ts
import { createContext } from '$lib/trpc/context';
import { router } from '$lib/trpc/router';
import type { Handle } from '@sveltejs/kit';
import { createTRPCHandle } from 'trpc-sveltekit';

// sequenceを使って複数のhandleを追加する例
export const handle: Handle = sequence(
  somthingHanfle,
  createTRPCHandle({ router, createContext })
);
```

### ヘルパー関数の作成
PageでtRPCを簡単に使用できるように、lib/trpc/client.tsにヘルパー関数を作成します。
```ts
import type { Router } from '$lib/trpc/router';
import { createTRPCClient, type TRPCClientInit } from 'trpc-sveltekit';

let browserClient: ReturnType<typeof createTRPCClient<Router>>;

export function trpc(init?: TRPCClientInit) {
  const isBrowser = typeof window !== 'undefined';
  if (isBrowser && browserClient) return browserClient;
  const client = createTRPCClient<Router>({ init });
  if (isBrowser) browserClient = client;
  return client;
}
```

## 関数を実行してみる
src/routes/+page.svelteに、以下のようなpageを作成します。
```svelte
<script lang="ts">
  import { page } from '$app/stores';
  import { trpc } from '$lib/trpc/client';

  let greeting = 'ボタンをクリックしてデータを取得';
  let loading = false;

  const loadData = async () => {
    loading = true;
    // tRPC Routerに作成した関数を実行する
    greeting = await trpc($page).greeting.query();
    loading = false;
  };
</script>

<h6>Loading data in<br /><code>+page.svelte</code></h6>

<a
  href="#load"
  role="button"
  class="secondary"
  aria-busy={loading}
  on:click|preventDefault={loadData}>Load</a
>
<p>{greeting}</p>
```

ボタンをクリックすると500msのディレイの後に「Hello tRPC」というメッセージが表示されます。

## 引数を渡してみる
Zodを使用して引数のスキーマを定義することができます。  
Router内の処理が実行される前に引数のバリデーションチェックも実施されます。
例として src/lib/trpc/router.ts を以下のように改変します。
```ts
// Zodスキーマ
const schema = z.object({
  message: .string().min(1)
});

export const router = t.router({
  // input関数を使ってスキーマを設定します
  greeting: t.procedure.input(schema).query(async ({ input }) => {
    ...
    // inputオブジェクトからスキーマの値を取得できます
    return `Hello tRPC @ ${input.message}}`;
  })
});
```
### 引数を指定して関数を実行
src/routes/+page.svelteも併せて改変します。
```svelte
<script lang="ts">
  ...

  const loadData = async () => {
    ...
    // tRPC Routerに作成した関数を実行する
    greeting = await trpc($page).greeting.query({ message: 'Japan' });
    ...
  };
</script>
...
```

ボタンをクリックすると「Hello tRPC @ Japan」というメッセージが表示されるようになりました。

## 情報の更新を伴う関数
query()はGETメソッドでリクエストされますが、POSTメソッドはmutation()という関数を使用します。  
mutationは、データの作成、更新、削除などを行うために使用されます。  
mutationを使用するためにsrc/lib/trpc/router.tsにmutation用のルーティングを追加してゆきます。
```ts
// Zodスキーマ
const schema = z.object({
  message: .string().min(1)
});

export const router = t.router({
  greeting: ...,
  postMessage: t.procedure.input(schema).mutation(async ({ ctx, input }) => {
    // 更新を伴う処理など

    // オブジェクトを返却することもできます
    return {
        id: ...
    }
  }
});
```

### Pageから実行する
実行の仕方はquery()と同じです。
```ts
const result = await trpc().createUser.mutate({
  message: 'Earth'
});

// 返り値も型安全です
// { id: xxxxxx }
result
```

## まとめ
以上で、SvelteKitとtRPCを使用する方法の説明となります。tRPCは、型安全な通信を提供し、開発者の生産性を向上させる素晴らしいツールです。trpc-sveltekitを使えば、SvelteKitとの統合も簡単に行えます。ぜひ、この組み合わせを試してみてください。

## 参考
https://kit.svelte.jp/

https://trpc.io/

https://github.com/icflorescu/trpc-sveltekit