Open8

OpenAPI to fetch to tRPC の意義

ebiyyebiyy

初期の要件

  • フロントエンドのモノレポで軽量なBFF的変換層を作成したい
  • バックエンドとの接続とフロントエンドで必要な型を返す
  • 初期要件にチーム開発で誰が描いても同じように

OpenAPI to fetch のアプローチ

  • OpenAPI仕様からTypeScript型定義とfetchベースのクライアントを生成
  • 型安全性と既存API仕様との互換性を確保

fetch to tRPC の検討

  • 生成されたfetchクライアントをtRPCルーターでラップする案
  • tRPCの型安全性とパフォーマンス最適化の利点を活用

tRPCの利点の認識

  • 完全な型安全性
  • クライアント-サーバー間の一貫性
  • 自動的なAPIクライアント生成
  • 効率的なクエリバッチング

実装の柔軟性

  • 全てのAPIをtRPCに移行する必要はない
  • 段階的な導入が可能
  • 既存のAPIと共存可能

クエリの非ブロッキング実行

  • tRPCのuseQueryによる自動的な並列クエリ実行
  • Promise.allを明示的に使用する必要がない

Server Actions vs tRPC

  • Server Actionsの順次実行の問題
  • tRPCによる効率的な並列クエリ実行

tRPCの内部最適化

  • React QueryのuseQueryを内部で最適化して使用
  • 型安全性、クエリキー管理、バッチリクエストの自動化

最終的な利点

  • 開発者体験の向上
  • 型安全性の強化
  • パフォーマンスの最適化
  • エラーハンドリングの改善
  • スケーラビリティと保守性の向上

考慮点

  • プロジェクトの規模と要件に応じた適切なアプローチの選択
  • チームの経験と学習曲線の考慮
  • 既存のインフラストラクチャとの統合
ebiyyebiyy

tRPCの利点の認識

  • 完全な型安全性
    • サーバーとクライアント間で型が共有され、コンパイル時にエラーを検出。
  • クライアント-サーバー間の一貫性
    • API定義が一箇所で管理され、整合性が保たれる。
  • 自動的なAPIクライアント生成
    • trpc.getUser.useQuery()のように、型安全なAPIクライアントが自動生成される。
  • チーム開発の一貫性
    • API定義が明確で、どの開発者も同じ方法でAPIを利用できる。
// サーバーサイド: API定義
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  getUser: t.procedure
    .input((val: GetUserParams) => val)
    .query(async ({ input }) => {
      // データベースからユーザーを取得するロジック
      return { id: input, name: 'John Doe', email: 'john@example.com' };
    }),
  createPost: t.procedure
    .input((val: CreateUserData) => val)
    .mutation(async ({ input }) => {
      // 新しい投稿を作成するロジック
      return { id: '1', ...input, createdAt: new Date() };
    }),
});

export type AppRouter = typeof appRouter;

// クライアントサイド: API使用
import { trpc } from '../utils/trpc';

const UserProfile = () => {
  const { data: user, isLoading } = trpc.getUser.useQuery('123');
  
  if (isLoading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;

  return <div>Welcome, {user.name}!</div>;
};

const CreatePostForm = () => {
  const createPost = trpc.createPost.useMutation();

  const handleSubmit = (title: string, content: string) => {
    createPost.mutate({ title, content });
  };

  // フォーム実装
};
ebiyyebiyy

tRPCの内部最適化

// tRPCを使用した並列クエリ実行
import { trpc } from '../utils/trpc';

const UserProfile = ({ userId }: { userId: string }) => {
  const { data: user } = trpc.getUser.useQuery(userId);
  const { data: posts } = trpc.getPosts.useQuery(userId);

  if (!user || !posts) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
};

// Server Actions を使用した場合(仮想的な例)
async function getUser(userId: string) {
  // ユーザー取得ロジック
}

async function getPosts(userId: string) {
  // 投稿取得ロジック
}

const ServerActionUserProfile = async ({ userId }: { userId: string }) => {
  const user = await getUser(userId);
  const posts = await getPosts(userId);  // userの取得が完了するまで開始されない

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
};

クエリの非ブロッキング実行

tRPCのuseQueryフックを使用すると、getUserとgetPostsクエリが自動的に並列で実行されます。
明示的にPromise.allを使用する必要がありません。tRPCとReact Queryが内部でこれを最適化します。

Server Actions vs tRPC

Server Actionsの例では、getUserとgetPostsが順次実行されます。getPostsはgetUserの完了を待ってから開始されます。
tRPCの例では、両方のクエリが同時に開始され、並列で実行されます。これにより、全体的なレスポンス時間が短縮されます。

tRPCの内部最適化

tRPCは内部でReact Queryを使用し、クエリキーの自動生成、キャッシュ管理、再フェッチの最適化を行います。
バッチリクエストの自動化:複数のクエリが同時に実行される場合、tRPCは可能な限りこれらをバッチ処理し、ネットワークリクエストを最小限に抑えます。

型安全性

tRPCは完全な型安全性を提供します。userとpostsの型が自動的に推論され、TypeScriptの恩恵を最大限に受けられます。

クエリキー管理

tRPCは各プロシージャに対して一意のクエリキーを自動的に生成します。これにより、キャッシュの一貫性が保たれ、不要な再フェッチを防ぎます。

最適化されたクエリの例

最後の例は、tRPCが内部でどのように最適化を行っているかを概念的に示しています。複数のクエリを1つの関数にまとめ、Promise.allを使用して並列実行しています。


// tRPCの内部最適化(概念的な例)
const optimizedTrpcQuery = createTRPCQuery({
  queryKey: ['user', userId],
  queryFn: async () => {
    const [user, posts] = await Promise.all([
      client.getUser.query(userId),
      client.getPosts.query(userId)
    ]);
    return { user, posts };
  },
});
ebiyyebiyy

まとめ

tRPCは、Next.jsの開発体験を大幅に向上させる多くの機能を提供しています:

型安全性

tRPCは、クライアントとサーバー間で完全な型安全性を自動的に提供します。これは、Next.js単体では手動で実装する必要がある機能です。

API定義と呼び出しの簡素化

tRPCを使用すると、APIエンドポイントの定義とクライアントサイドでの呼び出しが非常に簡単になります。Next.js単体では、これらを個別に実装し、型の整合性を手動で維持する必要があります。

自動的なクライアント生成

tRPCは、サーバーサイドの定義から自動的にクライアントを生成します。Next.jsでこれを実現するには、追加のツールや複雑な設定が必要です。

並列クエリの最適化

tRPCは、複数のクエリを自動的に最適化して並列実行します。Next.jsでこれを実現するには、開発者が明示的に実装する必要があります。

バッチリクエスト

tRPCは複数のクエリを自動的にバッチ処理し、ネットワークリクエストを最小限に抑えます。Next.js単体ではこの機能を手動で実装する必要があります。

統合されたキャッシュ管理

tRPCはReact Queryと統合されており、高度なキャッシュ管理機能を提供します。Next.jsでこれを実現するには、追加のライブラリと設定が必要です。

開発者体験の向上

tRPCは、型推論、自動補完、リアルタイムエラーチェックなどを通じて、開発者体験を大幅に向上させます。

フルスタック型安全性

tRPCは、フロントエンドとバックエンド間の完全な型安全性を提供し、開発プロセス全体を通じて一貫性を維持します。

結論として、tRPCは確かにNext.jsの開発において「欲しかった」多くの機能を提供しています。これらの機能は、TypeScript、React Query、そしてtRPC自体の革新的な設計を組み合わせることで実現されています。

ebiyyebiyy

Next.js + tRPC

https://trpc.io/docs/server/adapters/nextjs

tRPCのルーターは別々にかける。
pagesフォルダでも可能。

// src/server/trpc/router/user.ts
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
import { ApiClient } from '../../api-client';

const apiClient = new ApiClient({
  BASE: 'https://api.example.com',
});

export const userRouter = router({
  getUser: publicProcedure
    .input(z.string())
    .query(async ({ input }) => {
      return apiClient.user.getUser(input);
    }),
  getUserPosts: publicProcedure
    .input(z.string())
    .query(async ({ input }) => {
      return apiClient.user.getUserPosts(input);
    }),
});

// src/server/trpc/router/post.ts
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
import { ApiClient } from '../../api-client';

const apiClient = new ApiClient({
  BASE: 'https://api.example.com',
});

export const postRouter = router({
  createPost: publicProcedure
    .input(z.object({ title: z.string(), content: z.string() }))
    .mutation(async ({ input }) => {
      return apiClient.post.createPost(input);
    }),
});

// src/server/trpc/router/_app.ts
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';

export const appRouter = router({
  user: userRouter,
  post: postRouter,
});

export type AppRouter = typeof appRouter;

// クライアント側の使用例
// src/components/UserProfile.tsx
import { trpc } from '../utils/trpc';

export const UserProfile = ({ userId }: { userId: string }) => {
  const { data: user, isLoading: userLoading } = trpc.user.getUser.useQuery(userId);
  const { data: posts, isLoading: postsLoading } = trpc.user.getUserPosts.useQuery(userId);
  
  if (userLoading || postsLoading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <h2>Posts:</h2>
      <ul>
        {posts?.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
};
ebiyyebiyy

ex.
https://trpc.io/docs/getting-started
https://github.com/ferdikoomen/openapi-typescript-codegen?tab=readme-ov-file
https://github.com/t3-oss/create-t3-app/tree/abae9607a1a9600ac87a5c74aea62dfabe81161f?tab=readme-ov-file

// 1. 依存関係のインストール
// npm install openapi-typescript-codegen @trpc/server @trpc/client zod

// 2. OpenAPI仕様からFetchクライアントの生成
// openapi-typescript-codegen --input ./openapi.yaml --output ./src/api-client

// 3. 生成されたFetchクライアントをラップするtRPCルーターの作成
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
import { ApiClient } from './src/api-client';

const t = initTRPC.create();

const apiClient = new ApiClient({
  BASE: 'https://api.example.com',
});

export const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await apiClient.user.getUser(input.id);
      return user;
    }),
  createPost: t.procedure
    .input(z.object({ title: z.string(), content: z.string() }))
    .mutation(async ({ input }) => {
      const post = await apiClient.post.createPost(input);
      return post;
    }),
  // 他のAPIエンドポイントも同様にラップ
});

export type AppRouter = typeof appRouter;

// 4. フロントエンドでのtRPCクライアントの使用
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';

const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});

// クライアント側での使用例
async function fetchUser(id: string) {
  const user = await trpc.getUser.query({ id });
  console.log(user);
}

async function createNewPost(title: string, content: string) {
  const post = await trpc.createPost.mutate({ title, content });
  console.log(post);
}
ebiyyebiyy

補足

  • OpenAPI統合:tRPCはOpenAPI仕様と統合可能。
  • リアルタイム機能:WebSocketを用いたサブスクリプションをサポート。
  • ミドルウェア:認証、ロギングなどのカスタム処理が容易。
  • テスト性向上:サーバー・クライアント双方で一貫したテストが可能。
  • 最適化:効率的なデータシリアライズにより高速。
  • バージョニング:APIの新旧バージョン共存戦略が重要。
  • エラーハンドリング:型安全なエラー処理を提供。
  • 運用考慮:CORS設定やセキュリティに注意が必要。