はじめに
こんにちは。入社2年目、実務経験約半年の駆け出しフロントエンドエンジニアです。日々の業務では主にNuxt、Vue、AWS、TypeScriptを使って開発を行っています。
今回は、最近の開発で採用した「モノレポ構造」を通じて、型の共有がどのように役立つかについてお話しします。駆け出しのエンジニアの方にとって、少しでも参考になる情報をお届けできれば嬉しいです。
現在の開発環境
私の現在の開発環境は以下の通りです:
- フロントエンド: Nuxt 3
- バックエンド: AWS Lambda (Node.js)
- 共通言語: TypeScript
直面していた悩み
フロントとバックで別々のリポジトリに分かれていると、同じ型を使いたいのに、変更があるたびに手作業で同期しなければならず、とても面倒だなあと思っていました。
結果として、型の不一致が原因でエラーが発生したりして、開発スピードが落ちることもありました。
型共有の課題とモノレポ構造との出会い
フロントエンドとバックエンドで型を共有する一般的な方法としては、以下のようなものがあります:
- npm パッケージとして型定義を公開する
- Git サブモジュールを使用する
- API スキーマから型を自動生成する(例:OpenAPI, GraphQL)
これらの方法はそれぞれ利点がありますが、導入や運用に一定の複雑さと学習コストを感じました。(初学者なため)
そんな時に、「モノレポ構造」に出会いました。
モノレポ構造を知ったのは、開発に関する技術記事を読んでいるときでした。「フロントとバックで簡単に型定義を共有できる」という説明に興味を持ち、さらに調べてみると、私のような初心者でも比較的導入しやすそうだと感じました。
モノレポ構造とは
定義と基本的な概念
モノレポ(Monorepo)は「モノリシック・リポジトリ」の略で、複数のプロジェクトやパッケージを単一のリポジトリで管理する開発手法です。従来のマルチリポジトリ方式(各プロジェクトを別々のリポジトリで管理する方法)とは対照的に、モノレポでは関連するすべてのコードを1つのリポジトリにまとめます。
典型的なモノレポ構造は以下のようになります:
my-project/
├─ packages/
│ ├─ frontend/
│ │ └─ (Nuxt 3 プロジェクト)
│ ├─ backend/
│ │ └─ (Lambda 関数)
│ └─ shared/
│ └─ (共有するコードや型定義)
├─ package.json
└─ (その他の設定ファイル)
この構造では、packages
ディレクトリ内に各プロジェクトが配置され、shared
ディレクトリに共有するコードや型定義を置くことができます。
モノレポ構造の利点
-
コードの共有と再利用が容易
共通のコードや型定義を shared ディレクトリに置くことで、フロントエンドとバックエンドで簡単に共有できます。 -
一貫性の維持
プロジェクト全体で同じ依存関係やコーディング規約を適用しやすくなります。 -
原子的なコミットと改修
関連する変更を1つのコミットにまとめることができます。例えば、APIの変更とそれに伴うフロントエンドの修正を同時にコミットできます。 -
依存関係の管理の簡素化
プロジェクト全体の依存関係を一元管理できます。 -
新メンバーのオンボーディングが容易
プロジェクト全体の構造が1つのリポジトリで把握できるため、新しいチームメンバーが全体像を理解しやすくなります。
Nuxt 3とLambdaでのモノレポ構造の実装
プロジェクト構造の説明
my-project/
├─ packages/
│ ├─ frontend/
│ │ ├─ pages/
│ │ ├─ components/
│ │ └─ nuxt.config.ts
│ ├─ backend/
│ │ ├─ src/
│ │ │ └─ handlers/
│ │ └─ tsconfig.json
│ └─ shared/
│ └─ types/
│ ├─ user.ts
│ └─ api.ts
├─ package.json
└─ tsconfig.json
共有するコードの配置場所
共有するコードは packages/shared
ディレクトリに配置します。ここには主に型定義ファイルを置きますが、ユーティリティ関数なども配置可能です。
TypeScriptの型共有の実践
共有する型の定義方法
packages/shared/types/user.ts
の例:
export interface User {
id: string;
name: string;
email: string;
}
export interface UserResponse {
user: User;
token: string;
}
フロントエンドとバックエンドでの型の使用例
フロントエンド(Nuxt 3)での使用例:
// packages/frontend/pages/user.vue
import { User } from '../../shared/types/user';
const user: User = {
id: '1',
name: 'John Doe',
email: 'john@example.com'
};
バックエンド(Lambda)での使用例:
// packages/backend/src/handlers/getUser.ts
import { User, UserResponse } from '../../shared/types/user';
export const handler = async (event): Promise<UserResponse> => {
const user: User = {
id: '1',
name: 'John Doe',
email: 'john@example.com'
};
return {
user,
token: 'example-token'
};
};
モノレポ構造による開発効率の向上
コード重複の削減
共有の型定義を使用することで、フロントエンドとバックエンドで同じ構造を重複して定義する必要がなくなります。
型の一貫性維持の簡素化
型定義を1箇所で管理できるため、APIの変更があった場合でも、1箇所を修正するだけで済みます。
APIの変更に伴う影響の把握しやすさ
共有の型定義を変更すると、その型を使用しているすべての箇所でコンパイルエラーが発生するため、影響範囲が明確になります。
まとめ
モノレポ構造と型共有の主要な利点の再確認
- コードの共有と再利用が容易になる
- プロジェクト全体の一貫性が保たれる
- 開発効率が向上し、エラーを早期に発見できる
今後の展望
チームが若手のみなので、試行錯誤しながらモノレポベストプラクティクスの確立と改善をしていけたらと思っています。
おわりに
今回、私自身まだまだ駆け出しエンジニアとして、モノレポ構造を使ったNuxt 3とLambdaの開発経験を共有させていただきました。正直なところ、この方法が全ての状況で最適だとは思っていませんし、もっと効率的なアプローチがあると思っています。
そのため、経験豊富な先輩エンジニアの方々からご意見やアドバイスをいただけたら非常にありがたいです。「ここはこうしたほうがいいよ」とか「別の方法も考えられるね」といったコメントを大歓迎します。
私たち若手エンジニアにとって、現場で活躍されている方々の知見は本当に貴重です。この記事をきっかけに、様々な視点や経験を共有できる場になれば嬉しいです。
みなさんのフィードバックを楽しみにしています。一緒により良い開発方法を探っていけたらと思います!