Open2

TypeScriptのFrontEndと、BackEndで、型を共有するためのsharedディレクトリの作成について📝

まさぴょん🐱まさぴょん🐱

TypeScriptでフロントエンドとバックエンド間で型を共有するためにsharedディレクトリを作成するのは、コードの再利用性と一貫性を保つために有効です📝

以下に、その設定や仕組みを実装するための具体的な手順と考慮点を説明します。

1. プロジェクト構成の設計

まず、モノレポ形式(単一リポジトリ内でフロントエンドとバックエンドを管理)か、別々のリポジトリにするかを決めます。ここではモノレポを前提に話を進めますが、別リポジトリの場合も同様の考え方を適用できます。

ディレクトリ構造の例

project-root/
├── frontend/         # フロントエンド用のディレクトリ
│   ├── src/
│   ├── tsconfig.json
│   └── package.json
├── backend/          # バックエンド用のディレクトリ
│   ├── src/
│   ├── tsconfig.json
│   └── package.json
├── shared/           # 型やユーティリティを共有するディレクトリ
│   ├── types/        # 型定義
│   │   └── index.ts
│   ├── package.json  # 必要に応じて
│   └── tsconfig.json # 必要に応じて
└── package.json      # ルートレベルでの依存関係管理
  • shared/: フロントエンドとバックエンド間で共有する型やユーティリティを格納。
  • frontend/: ReactやVueなどフロントエンドのコード。
  • backend/: Node.jsやExpressなどバックエンドのコード。

2. sharedディレクトリの設定

sharedディレクトリをモジュールとして扱い、フロントエンドとバックエンドからインポートできるようにします。

(1) shared/types/index.ts の作成

共有したい型を定義します。例えば、APIのレスポンス型やエンティティをここに記述します。

// shared/types/index.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export type ApiResponse<T> = {
  data: T;
  status: 'success' | 'error';
  message?: string;
};

(2) package.json の設定(任意)

sharedをローカルパッケージとして扱う場合、shared/package.jsonを作成します。

{
  "name": "@myproject/shared",
  "version": "1.0.0",
  "main": "types/index.ts",
  "types": "types/index.ts"
}

ルートレベルのpackage.jsonに依存関係として追加します。

{
  "dependencies": {
    "@myproject/shared": "file:./shared"
  }
}

その後、npm installまたはyarn installでリンクします。

3. TypeScriptの設定(tsconfig.json

フロントエンドとバックエンドがsharedディレクトリを正しく参照できるように、tsconfig.jsonを調整します。

(1) パスの設定

sharedをモジュールとしてインポートしやすくするために、TypeScriptのpathsオプションを使用します。

ルートのtsconfig.json(ベース設定)

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@shared/*": ["shared/*"]
    }
  }
}

frontend/tsconfig.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*", "../shared/**/*"]
}

backend/tsconfig.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*", "../shared/**/*"]
}
  • @shared/*: sharedディレクトリを@sharedというエイリアスで参照可能に。
  • include: sharedディレクトリを明示的に含める。

(2) モジュール解決

pathsを使う場合、ビルドツール(Webpack, Vite, ts-nodeなど)でもパス解決を設定する必要があります。

  • Webpackの場合: tsconfig-paths-webpack-pluginを利用。
  • Viteの場合: vite-tsconfig-pathsプラグインを利用。
  • Node.jsの場合: tsconfig-pathsパッケージを追加。

4. ビルドとデプロイの考慮

  • sharedの型をビルドするかどうか: 型定義のみの場合はビルド不要ですが、ユーティリティ関数を含む場合はビルドが必要です。その場合、sharedに独自のtsconfig.jsonを用意し、tscでコンパイルします。
  • 依存関係の管理: フロントエンドとバックエンドで異なる依存関係がある場合、sharedは依存関係を最小限に抑える設計に。

5. 実際の利用例

フロントエンドでの使用

// frontend/src/App.tsx
import { User, ApiResponse } from '@shared/types';

const fetchUser = async (): Promise<ApiResponse<User>> => {
  const response = await fetch('/api/user');
  return response.json();
};

バックエンドでの使用

// backend/src/server.ts
import { User, ApiResponse } from '@shared/types';

const getUser = (): ApiResponse<User> => {
  return {
    data: { id: 1, name: 'Taro', email: 'taro@example.com' },
    status: 'success'
  };
};

6. 注意点とベストプラクティス

  • 型のシンプルさ: sharedには複雑なロジックを含めず、型定義や軽量なユーティリティに限定。
  • バージョニング: 大規模プロジェクトでは、sharedを独立したパッケージとしてNPMに公開するのも検討。
  • テスト: 型の整合性を保つため、フロントエンドとバックエンドでAPIのモックテストを用意。
まさぴょん🐱まさぴょん🐱

Prismaの型定義をFrontEndとBackEndで共有する場合のアプローチ📝

Prismaの型定義をFrontEndとBackEndで共有する場合、効率的かつメンテナンスしやすい方法を選ぶことが重要です。
以下に、ベストプラクティスとして考えられるアプローチをいくつか挙げます。状況に応じて最適なものを選ぶか、組み合わせるのが良いでしょう。

1. モノレポを利用する

  • 概要: FrontEndとBackEndを同一リポジトリ(モノレポ)で管理し、Prismaのスキーマや生成された型を共有パッケージとして扱う。
  • やり方:
    1. prisma ディレクトリをルートに配置し、schema.prisma を定義。
    2. npx prisma generate で型を生成(例: prisma/generated/client)。
    3. 生成された型を、@my-org/prisma-types のような共有パッケージとしてエクスポート。
    4. FrontEndとBackEndのそれぞれのプロジェクトで、このパッケージをインポートして利用。
  • メリット:
    • 型定義の一元管理が可能。
    • モノレポツール(Nx, Turborepoなど)を使えば依存関係の管理が楽。
  • デメリット:
    • モノレポのセットアップや運用に慣れが必要。

2. 型定義を独立したパッケージとして公開

  • 概要: Prismaの型を別リポジトリやプライベートnpmパッケージとして切り出し、FrontEndとBackEndでそれをインポートする。
  • やり方:
    1. Prismaスキーマ専用のリポジトリを作成。
    2. prisma generate で型を生成し、パッケージ化(例: my-prisma-types)。
    3. npmやYarnで公開(プライベートならGitHub Packagesや自前のレジストリを使用)。
    4. FrontEndとBackEndの package.json に依存関係として追加。
  • メリット:
    • プロジェクトが分離していても共有が簡単。
    • バージョン管理が明確。
  • デメリット:
    • パッケージの更新とデプロイの手間が増える。

3. 型定義を手動でコピーして同期

  • 概要: BackEndで生成した型定義をFrontEndにコピーするスクリプトを作成し、手動またはCIで同期。
  • やり方:
    1. BackEndで prisma generate を実行。
    2. 生成された型(例: node_modules/.prisma/client/index.d.ts)をFrontEndの特定ディレクトリにコピー。
    3. 必要に応じて、rsync やカスタムスクリプトで自動化。
  • メリット:
    • シンプルでモノレポやパッケージ公開が不要。
  • デメリット:
    • 同期ミスや手動作業が発生する可能性。

4. tRPCやGraphQLを介して間接的に共有

  • 概要: Prismaの型を直接共有するのではなく、tRPCやGraphQLのようなAPIレイヤーで型を統合し、FrontEndがその型を利用する。
  • やり方:
    • tRPCの場合:
      1. BackEndでPrismaを使ってtRPCルーターを定義。
      2. tRPCが生成する型をFrontEndで利用(例: @trpc/client)。
    • GraphQLの場合:
      1. Prismaのモデルを基にGraphQLスキーマを定義。
      2. GraphQL CodegenでFrontEnd用の型を生成。
  • メリット:
    • API経由で型が自然に共有される。
    • 型安全なエンドツーエンドの開発体験。
  • デメリット:
    • tRPCやGraphQLの導入コストが高い。

おすすめのベストプラクティス

  • 小規模プロジェクト: 「型定義を手動でコピー」が最もシンプルで手軽。
  • 中規模〜大規模プロジェクト: 「モノレポ」を採用し、型を共有パッケージとして管理するのがメンテナンス性とスケーラビリティのバランスが良い。
  • 型安全性を重視する場合: 「tRPC」を導入すると、Prismaの型を直接意識せずともFrontEndとBackEndで一貫性が保てる。

個人的には、モノレポ + tRPC の組み合わせが、型共有の煩雑さを減らしつつ、開発体験を向上させるので特におすすめです。例えば、以下のような構成が考えられます:

monorepo/
├── packages/
│   ├── prisma/          # Prismaスキーマと型生成
│   ├── backend/        # tRPCサーバー実装
│   └── frontend/       # tRPCクライアント利用

注意点

  • Prismaの型は巨大になりがちなので、必要な型だけをエクスポートするように工夫する(例: Pick やカスタム型で絞り込み)。
  • CI/CDで型生成と同期を自動化すると、手動ミスが減る。

プロジェクトの規模やチームのスキルセットに合わせて選んでみてください。何か具体的な制約があれば教えてください、さらに掘り下げて提案します!