頑張りすぎないプロジェクト立ち上げのすゝめ

## はじめに

新しいプロジェクトの立ち上げ——エンジニアにとって期待と不安が入り混じる瞬間ですよね。
「何から手をつけるべきか」「どこまでやれば十分なのか」という明確な正解がない中で、
初期設定をおざなりにすると後々の開発で苦労する「技術的負債の種まき」となってしまいます。

本記事では、プロジェクト立ち上げ時に最低限考慮すべき要素と、その実践的なアプローチを解説します。
見落としがちなポイントを押さえ、「頑張りすぎない」バランスの取れた初期設定の考え方をお伝えします。

## プロジェクト立ち上げ時の課題

「どこまで準備すれば十分なのか」——この問いに悩むエンジニアは少なくありません。
新規プロジェクトの立ち上げ時、私たちは以下のような多くの選択肢に直面します：

- 適切なデザインパターンの選定
- 将来を見据えたディレクトリ構造の設計
- 堅牢なエラーハンドリング戦略
- 一貫性を保つための静的解析・フォーマットルール
- チーム開発を円滑にするGitフロー
- その他、無数の技術的決断...

この悩みは、以下のような状況ではさらに深刻になります：

- 未経験の技術スタックに挑戦している場合
- プロジェクトリードの経験が浅い場合
- チームメンバーのスキルレベルにばらつきがある場合
- プロジェクト開始までの準備時間が限られている場合

こうした状況では、「完璧な準備」を目指すあまり、肝心のプロダクト開発が遅れてしまうリスクもあります。

## この記事で伝えたいこと—「頑張りすぎない」ための3つの指針

「頑張りすぎない」初期設定とは、必要最小限でありながら、後の開発を円滑にする土台作りです。
本記事では、以下の3つの実践的な指針を提案します：

1. **エラーハンドリングの基本方針を決めて共有する**
— デバッグの効率化と問題の早期発見につながる基盤
2. **自動化ツールを導入してコード品質を担保する**
— lintとformatの自動化で、レビュー負担を軽減し一貫性を確保
3. **既存のベストプラクティスを活用する**
— デザインパターンやディレクトリ構造はフレームワークの思想に則り、車輪の再発明を避ける

これらの指針は、「完璧を目指す」のではなく「必要十分な準備」を重視しています。
まずはビジネス価値の創出を優先し、プロジェクトの成長に合わせて段階的に機能や設計を洗練させていくアプローチが、長期的には効果的です。

## 最低限の準備をしないと...技術的負債の罠

とはいえ、全く頑張らないで「後で直せばいい」という考えが、プロジェクトを思わぬ苦境に追い込むことがあります。
初期設定を疎かにした場合、以下のような具体的な問題が徐々に表面化してきます：

### 1. コードの一貫性が失われる

**具体例：** チームの各メンバーが独自のコーディングスタイルを採用し始めます。
あるファイルではキャメルケース、別のファイルではスネークケース。
インデントはタブだったり、スペース2つだったり、4つだったり...。
こうした不統一は、コードの可読性を著しく低下させ、新メンバーの学習コストを増大させます。

```tsx
// 開発者Aによるコード
interface User {
  id: number;
  name: string;
  email: string;
}

async function getUserData(userId: number): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);
  return response.json() as Promise<User>;
}

// 開発者Bによるコード
type user_data = {
  id: number,
  name: string,
  email: string,
}

function get_user_data (user_id: number): Promise<user_data> {
    return fetch('/api/users/' + user_id)
        .then(function(res) { return res.json() as Promise<user_data>; });
}

```

### 2. デバッグが困難になる

**具体例：** エラーハンドリングの方針が未定義のため、あるモジュールではエラーをログに出力し、
別のモジュールでは例外を投げ、また別のモジュールでは静かに失敗します。
本番環境で問題が発生した際、ログには「エラーが発生しました」としか記録されておらず、
原因特定に数時間を要することになります。

### 3. 開発速度の低下

**具体例：** プロジェクト開始時に採用した複雑なデザインパターンに、チームの大半が不慣れでした。
単純なCRUD操作を実装するのに、本来30分で済むところ、パターンに合わせるために3時間かかるようになります。
結果として、予定していた機能の半分しか納期までに実装できませんでした。

---

これらの問題は、プロジェクトが進行するにつれて**雪だるま式に拡大**します。
後から修正しようとすると：

- 影響範囲の調査だけで数日を要する
- 修正によって新たなバグが発生するリスクが高まる
- チーム全体の再教育が必要になる

レビューだけでこれらの問題をカバーしようとすると、レビュアーの負担が増大し、
本来の機能開発に充てるべき時間が大幅に削られてしまいます。

**最も重要なのは、これらの問題が「目に見えないコスト」として蓄積されていくこと**です。
プロジェクトの初期段階では気づきにくいものの、後になって大きな障壁となって立ちはだかります。

## 最低限の準備としてやるべきこと—「頑張りすぎない」実践ガイド

### 1. エラーハンドリングの基本方針—シンプルながらも強力な土台

「エラーが発生したらどう対処すべきか」—この問いに対する明確な指針がないまま開発を進めると、前章で見たような混乱が生じます。
しかし、複雑な例外階層を設計する必要はありません。シンプルながらも効果的なアプローチを採用しましょう。

#### 基本方針—シンプルな2ステップ

エラーハンドリングの基本は、次の2つのステップだけです：

1. **ログに具体的な内容を出力する**
— 問題解決に必要な情報を残す
2. **アプリケーション例外を投げる**
— 一貫した方法でエラーを伝播させる

この2ステップを徹底するだけで、デバッグの効率は劇的に向上します。

#### ログに含めるべき情報—デバッグの鍵

「エラーが発生しました」だけでは、原因究明に時間がかかります。効果的なログには以下の情報を含めましょう：

- **発生日時** — いつ問題が起きたのか
- **発生箇所** — どのコンポーネント・関数で問題が発生したのか
- **操作データのID** — どのデータを処理中に問題が発生したのか
- **エラーの種類と詳細** — 何が原因で問題が発生したのか

```tsx
// 悪い例
logger.error('エラーが発生しました');

// 良い例
logger.error('ユーザー登録処理でエラーが発生しました', {
  userId: '12345',
  email: 'user@example.com', // 個人情報はマスク処理を検討
  timestamp: new Date().toISOString(),
  stack: error.stack
});

```

> 注意点: 個人情報（氏名、電話番号、パスワードなど）をログに出力する場合は、必要に応じてマスク処理を行いましょう。
> 

#### アプリケーション例外—一貫性のあるエラー処理の要

アプリケーション例外とは、プロジェクト固有のエラー情報を扱うための独自例外クラスです。
複雑な継承階層は必要ありません。以下の情報を含む単一のクラスから始めましょう：

- **エラーメッセージ** — ユーザーやデベロッパーに表示する情報
- **ステータスコード** — HTTPレスポンスの状態コード
- **コンテキスト情報** — デバッグに役立つ追加データ

例外処理は、アプリケーションの入口（APIコントローラーなど）で一元的に行うことで、コードの重複を避けられます。
ただし、以下のケースでは例外処理を行った後に再度例外を投げることが適切です：

- **トランザクション処理** — ロールバックが必要な場合
- **リソース解放** — ファイルハンドルやDBコネクションのクローズが必要な場合
- **その他のクリーンアップ** — 一時ファイルの削除など

#### 実装例—理論から実践へ

ここでは、NestJSを使用した実装例を通して、シンプルながらも効果的なエラーハンドリングの実現方法を見ていきましょう。

> ポイント: この実装は「完璧」を目指すものではなく、「必要十分」な出発点です。プロジェクトの成長に合わせて拡張していくことができます。
> 

```tsx
// 1. アプリケーション例外の定義 - シンプルな単一クラス
export class ApplicationException extends Error {
  constructor(
    public readonly message: string,        // ユーザー向けメッセージ
    public readonly statusCode: number = 400, // デフォルトは400 Bad Request
    public readonly context?: Record<string, any> // デバッグ用コンテキスト情報
  ) {
    super(message);
  }
}

// 2. サービスでのエラーハンドリング実装例
@Injectable()
export class UserService {
  private readonly logger = new Logger(UserService.name);

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly dataSource: DataSource
  ) {}

  async createUser(userData: CreateUserDto): Promise<User> {
    try {
      // 通常の処理フロー
      // ここではトランザクション処理などが入ります
    } catch (error) {
      // ステップ1: 適切なログ出力
      this.logger.error('ユーザー登録中にエラーが発生しました', {
        error: error.message,
        userData: { email: userData.email }, // 個人情報は最小限に
        timestamp: new Date().toISOString(),
        stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined
      });

      // ステップ2: エラーの種類に応じた例外を投げる
      if (error instanceof QueryFailedError) {
        if (error.message.includes('duplicate key')) {
          throw new ApplicationException(
            'このメールアドレスは既に登録されています', // 明確なメッセージ
            400, // 適切なステータスコード
            { email: userData.email } // コンテキスト情報
          );
        }
      }

      // その他の予期せぬエラー
      throw new ApplicationException(
        'ユーザー登録中にエラーが発生しました',
        500 // サーバーエラー
      );

    } finally {
      // リソース解放などの後片付け
    }
  }
}

// 3. 例外フィルターによる一元的な処理
@Catch(ApplicationException)
export class ApplicationExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(ApplicationExceptionFilter.name);

  catch(exception: ApplicationException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // 追加のログ出力（監査目的など）
    this.logger.error(`[${new Date().toISOString()}] Error:`, {
      path: request.url,
      method: request.method,
      message: exception.message,
      statusCode: exception.statusCode,
      context: exception.context
    });

    // 一貫したエラーレスポンス形式
    response.status(exception.statusCode).json({
      status: 'error',
      message: exception.message,
      context: {
        ...exception.context,
        timestamp: new Date().toISOString()
      }
    });
  }
}

// 4. モジュールでの例外フィルター登録
@Module({
  providers: [
    UserService,
    {
      provide: APP_FILTER,
      useClass: ApplicationExceptionFilter,
    },
  ],
})
export class AppModule {}

```

**このシンプルな実装がもたらす効果：**

1. **デバッグの効率化** — 詳細なログにより問題の原因特定が容易に
2. **一貫したエラー処理** — フロントエンドが予測可能な形式でエラーを受け取れる
3. **拡張性** — 必要に応じて例外クラスを拡張できる基盤ができる

> 実装のポイント: 完璧なエラーハンドリングを最初から目指すのではなく、この基本形から始めて、プロジェクトの要件に応じて段階的に拡張していくアプローチが効果的です。
> 

### 2. 自動化ツールでコード品質を担保する—レビュー地獄からの解放

「なぜビルドに失敗するの？」「なぜ使っていない変数があるの？」
コミット前にビルドが通ることを確認し、フォーマッタをかけるというルールがドキュメント上にあったとしても、人間誰しもミスを犯します。
静的解析とフォーマットの自動化は、こうした単純な指摘から開発者とレビュアーを解放し、より本質的なコードレビューに集中できる環境を作ります。

以下では、TypeScriptプロジェクトで効果的な自動化ツールの組み合わせを紹介します。

#### Git Hooks—コミット前の品質チェック自動化

Git Hooksとは、Gitの特定のイベント（コミット前、プッシュ前など）で自動的に実行されるスクリプトです。
これを活用することで、品質の低いコードがリポジトリに混入することを未然に防げます。

> 問題点: 標準のGit Hooksはローカル設定のため、チーム全体での統一が難しい
> 

#### Lefthook—チーム全体で統一されたGit Hooks管理

[Lefthook](https://github.com/evilmartians/lefthook)は、Git Hooksをプロジェクト全体で共有・管理するためのツールです。
設定ファイルをバージョン管理することで、チーム全員が同じ品質チェックを適用できます。

```yaml
# プロジェクトルートに配置するlefthook.yml
pre-commit:  # コミット前に実行
  parallel: true  # 並列実行で高速化
  commands:
    lint:
      glob: "*.{ts,tsx}"  # TypeScriptファイルのみ対象
      run: biome lint {staged_files}
    format:
      glob: "*.{ts,tsx,js,jsx,json}"
      run: biome format --write {staged_files}
      stage_fixed: true  # 修正されたファイルを自動的に再ステージング

```

#### Biome—高速で統合されたTypeScriptツールチェーン

[Biome](https://biomejs.dev/)は、TypeScript/JavaScriptのための高速な静的解析・フォーマットツールです。
ESLint+Prettierの代替として、以下の利点があります：

1. **高速な実行** — Rustで実装されており、大規模プロジェクトでも高速
2. **設定の簡素化** — 一つのツールで静的解析とフォーマットを実現

```json
{
  "$schema": "<https://biomejs.dev/schemas/1.9.4/schema.json>",
  "organizeImports": {
    "enabled": true //importの自動ソート
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedVariables": "error", //未使用変数のエラー
        "noUnusedImports": "error" //未使用importのエラー
      },
      "style": {
        "useImportType": "off", //type用途のimportをimport typeに自動変換しない
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2
  }
}

```

> 実装のポイント: 最初から厳格なルールを全て適用するのではなく、まずは基本的なルールから始めて、チームの習熟度に合わせて段階的に厳格化していくアプローチが効果的です。
> 

### 3. 既存のベストプラクティスを活用する—車輪の再発明を避ける

「クリーンアーキテクチャを導入しよう」「DDDで設計しよう」—新しいプロジェクトを始める際、こうした高度な設計手法の導入を検討することがあります。
実際いくつか書籍を読んで触れてみると理には適っているし、できることであれば導入したいと考えています。
しかし、これらの手法は本当に**今すぐの導入**がベターかどうかはよく考えた方が良いです。

#### 高度なデザインパターンの「コスト」を考える

クリーンアーキテクチャやDDDなどの高度なデザインパターンには、以下のような「隠れたコスト」が存在します：

- **学習コスト** — チームメンバー全員が概念を理解するための時間
- **実装コスト** — より複雑な構造を実装するための追加工数
- **維持コスト** — 新メンバーへの教育や、パターンを維持するための継続的な努力

これらのコストは、特にチームにこれらのパターンの経験者が少ない場合、想像以上に大きくなります。
私は実際に、学習途中のDDDを導入しようとして1ヶ月の手戻りを発生させました。ごめんなさい。

#### フレームワークの思想に則る—「適切な退屈さ」の価値

多くの現代的なフレームワークは、長年の経験から洗練された設計思想を持っています：

- **NestJS** — モジュール、コントローラー、サービスの明確な分離
- **Ruby on Rails** — 「Convention over Configuration」の思想
- **Laravel** — 「The PHP Framework for Web Artisans」、表現力豊かなEloquent ORMと優れた抽象化

これらのフレームワークが提供する基本構造に従うことで、以下のメリットが得られます：

1. **ドキュメントとの一致** — 公式ドキュメントやチュートリアルがそのまま適用可能
2. **コミュニティの知見** — 同じ構造を使う多くの開発者の知見を活用できる
3. **採用市場の広さ** — 一般的な構造のため、新メンバーの参入障壁が低い

もちろん賛否が分かれているものもありますが、賛否の多さは知見の多さです。
自分で使ってみて不便な点や変えたい点が出てきた時にその問題を解消できるデザインパターンや設計に変更するのが良いでしょう。

#### 段階的な進化—ビジネス価値を優先する

「完璧な実装」よりも「適切なタイミングでの進化」の方を重要視すべきです。
もちろん、プロジェクトの背景によっては優先順位は変わります。
しかし、ビジネス開発の本質を考えると、
「完璧な実装がされたお金を稼がない」よりも、「完璧ではないがお金を稼ぐシステム」を優先すべきでしょう。最終的には「完璧な実装でお金を産むシステム」を目指すべきですが、甘くないですね。

**初期段階では：**

- フレームワークの基本構造（MVC、モジュール構造など）に従う
- 過度な抽象化を避け、具体的な実装から始める
- ビジネス価値の早期提供を優先する

**プロジェクトの成長に合わせて：**

- 繰り返し現れるパターンを特定し、適切な抽象化を導入
- チームの知識レベルに合わせて、段階的に設計を洗練させる
- リファクタリングの時間を定期的に確保する

そもそも「完璧」なんてものは無いと思います。
技術負債を産んだことのない者が、技術負債に石を投げることができます。

> 実践的アドバイス: 新しいプロジェクトでは、最初の3ヶ月は「フレームワークの流儀に従う」ことを徹底し、その後のふりかえりでアーキテクチャの評価と進化の方向性を決めるアプローチが効果的です。
> 

## まとめ—「頑張りすぎない」初期設定の本質

新しいプロジェクトの立ち上げは、無限の可能性と選択肢の海です。その中で何を優先すべきか—本記事では「頑張りすぎない」初期設定の考え方を紹介してきました。

最も重要なのは、**完璧を求めすぎないこと**です。初期段階で全てを完璧に設計することは不可能であり、むしろビジネス価値の創出を遅らせるリスクがあります。

代わりに、以下の3つの指針に集中することをお勧めします：

1. **エラーハンドリングの基本方針を決める**
— シンプルな例外クラスと一貫したログ出力で、後のデバッグを容易にします。複雑な例外階層は必要ありません。
2. **自動化ツールでコード品質を担保する**
— Git HooksとLefthook、Biomeなどのツールを活用し、コードの一貫性を自動的に確保します。人間の記憶に頼るのではなく、システムに任せましょう。
3. **既存のベストプラクティスを活用する**
— フレームワークの基本構造に従い、車輪の再発明を避けます。高度なデザインパターンは、必要に応じて段階的に導入していくことが効果的です。

これらの準備は、後から変更するのが難しいため、プロジェクト開始時にしっかりと決めておくことが重要です。しかし、それ以上に重要なのは、**ビジネス価値の早期提供**と**段階的な進化**のバランスです。

完璧を目指すのではなく、適切なタイミングで適切な改善を行っていく—それが「頑張りすぎない」初期設定の本質です。

最後に、どんなプロジェクトも時間の経過とともに進化します。今日の最適解が明日も最適とは限りません。大切なのは、チームで定期的に振り返り、必要に応じて設計を見直す習慣を持つことです。そして何より、ユーザーに価値を届けることを最優先に考えられる技術者を目指しましょう。