弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

Full-Stack TypeScriptで型安全な開発サイクルを実現! ── Legal Brain 開発事例

こんにちは! Legal Brain エージェントを開発している下山です。

Legal Brain エージェントはリーガル特化型 AI エージェントで、法律事務所や企業の法務部門での複雑なリサーチ業務を支援するリーガルリサーチ機能を提供しています。

このプラットフォームはマイクロサービス構成で、フロントエンドとバックエンドに TypeScript を採用することで開発サイクルを大幅に向上させました。本記事では、その実践事例をご紹介します。

プロジェクト概要と課題

Legal Brain エージェントではバックエンド(Go)とフロントエンド(TypeScript)で異なる言語を使用していたため、型定義の不整合や開発効率の低下といった課題に直面していました。

開発で直面した課題

バックエンド・フロントエンド間の型定義の不一致

異なる言語で開発することで、バックエンドとフロントエンド間で型定義の整合性を保つことが困難でした。

// バックエンドで定義した型
type User struct {
    ID    string `json:"id"`
    Email string `json:"email"`
}
// フロントエンドで手動で定義した型
interface User {
  id: string;
  name: string; // 実際のレスポンスに存在しないプロパティ
  email?: string; // 必須フィールドがoptionalとして定義されている
}

このような型定義の不一致により、実行時に予期しないエラーや不要な if 文が発生し、開発に多くの時間を費やすことになりました。

API仕様変更時の手動作業の多さ

従来の開発フローでは、API 仕様が変更されるたびに以下の手順が必要でした:

  1. バックエンドで API を実装
  2. フロントエンドで型定義を手動作成
  3. 型の不整合を発見・修正

この手順を繰り返すことで、開発速度が大幅に低下していました。

開発者の認知負荷と体験の低下

バックエンド(Go)とフロントエンド(TypeScript)で異なる言語を使用し、型定義が二重管理になっていたことで、開発者は「この定義ってどっちが正しいんだっけ?」と都度確認しなければなりませんでした。

その結果、本来の開発に集中できる時間が減り、以下のような開発体験上の問題に繋がっていました:

  • IDE補完の問題: 型定義が不正確なため、適切な補完が機能しない
  • リファクタリングが困難: 型の影響範囲が不明確で、安全なリファクタリングができない
  • ランタイムエラーの増加: 実行してはじめてエラーが発生し、修正に時間を取られる
  • 学習コストの高さ: Go と TypeScript を両方学ぶ必要があり、少人数チームでは特に負担になる

技術スタックの移行と選定理由

こういった課題を解決するために、技術スタックを移行することにしました。移行前の技術スタックは以下のとおりです。

  • バックエンド: Go
  • フロントエンド: Vite + React

移行後の技術スタックは以下のようになりました。

  • バックエンド: NestJS
  • フロントエンド: Next.js
  • APIスキーマ定義: ts-rest + Zod
  • 共有ライブラリ: モノレポ構成(pnpm workspace)

Full-Stack TypeScriptを選んだ理由

リリースまでの学習コスト削減

少人数のチームで Go と TypeScript の両方に精通する必要があり、開発者の負担が大きかったため、同じ言語(TypeScript)を使用することで学習コストを抑えることにしました。

型定義の共有

バックエンドとフロントエンドで同じ言語を使用することで、型定義を共有しやすくなります。これにより、API の入出力の型を両方で同じように扱えるようになりました。

開発効率の向上

同じ言語を使用することで、開発者がバックエンド・フロントエンド両方のコードを理解しやすく、開発効率が向上しました。

フレームワーク選定の理由

フレームワークとして、バックエンド側には NestJS を、フロントエンド側には Next.js を採用することにしました。その選定理由は以下のとおりです。

NestJSを選んだ理由

  • 豊富なドキュメント: 公式ドキュメントが充実しており、学習コストを最小限に抑えられる
  • エンタープライズ向け: 大規模アプリケーション開発に適した設計思想
  • DIコンテナ: 依存性注入により、テストしやすく保守性の高いコードを書ける
  • モジュール化: 機能ごとにモジュールを分割し、コードの整理が容易

Next.jsを選んだ理由

  • 豊富なドキュメント: 公式ドキュメントとコミュニティの情報が充実
  • SSR/SSG対応: SEO とパフォーマンスの両立が可能
  • App Router: 最新の React 機能を活用した直感的なルーティング
  • TypeScript対応: 型安全性を保ちながら開発可能

型安全性を支えるts-restの採用

Full-Stack TypeScript の利点を最大限に活かすため、型安全な API を定義できる ts-rest を採用しました。

ts-restとは

ts-rest は、TypeScript で型安全な API を定義し、バックエンドとフロントエンド間で型を共有できるライブラリです。TypeScript の型システムを活用して API の型安全性を保証します。

検討した技術とts-restを選んだ理由

API 型定義の技術として、OpenAPI と GraphQL も検討しましたが、最終的に ts-rest を選択しました。

技術 説明 選ばなかった理由
OpenAPI RESTful API の仕様を定義する一般的な方法 • Full-Stack TypeScript 環境で OpenAPI 仕様書を経由する必要性が低い
GraphQL クエリ言語とランタイムを提供する API 仕様 • チーム全体の学習コストが高い
• 現在の要件に対して機能が過剰
ts-rest TypeScript で型安全な API を定義するライブラリ -

ts-restの利点:

  • 直接的な型共有: TypeScript の型定義を直接共有できる
  • 開発体験の向上: 型定義の変更が即座に反映される
  • Zodスキーマの統一: バリデーションと型定義を一箇所で管理
  • 学習コストの低さ: 既存の TypeScript 知識で十分対応可能
  • 将来の柔軟性: OpenAPI スキーマの出力をサポートしているため、将来的に他の言語に移行する際も対応可能

Zodスキーマによる型定義とバリデーションの統一

ts-rest では、API のスキーマとバリデーターを Zod で定義できます。これにより、バックエンド・フロントエンド両方で同じスキーマを共有できます。

従来の開発:

// バックエンド(Go)
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

// バリデーションは別途実装が必要
// フロントエンド(TypeScript)
interface User {
  id: string;
  name: string;
}

// バリデーションは別途実装が必要

ts-rest + Zod での開発:

// 共通バリデーションスキーマ(バックエンド・フロントエンド両方で使用)
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});

// バリデーションスキーマから型へ変換
type User = z.infer<typeof UserSchema>;

この方法により、型定義とバリデーションロジックを一箇所で管理でき、バックエンド・フロントエンド両方で同じスキーマを共有できます。

開発フローの改善

従来の開発フローは、以下のとおりでした。

  1. バックエンドで API 実装
  2. フロントエンドで型定義を手動作成
  3. 型の不整合を発見・修正

Full-Stack TypeScript での開発フローは、以下のようになります。

  1. 共通スキーマで API 仕様定義
  2. バックエンド・フロントエンド同時実装
  3. 型安全性が自動的に保証される

この変更により、手戻りを減らし開発効率が向上することを期待しました。

アーキテクチャ設計と実装

モノレポ構成による効率的な開発環境

開発効率を最大化するため、モノレポ構成を採用しました:

project-root/
└── apps/
    ├── frontend/    # Next.js アプリケーション
    ├── backend/     # NestJS アプリケーション
    └── common/      # 共有ライブラリ(型定義・スキーマ)

この構成により、シンプルにバックエンドとフロントエンドで型定義を共有し、開発効率を大幅に向上させることができました。

型安全なAPI設計の実現

Full-Stack TypeScript の利点を活かし、ts-rest を使用してバックエンドとフロントエンド間で型安全な API を定義しました。

API 実装例

NestJS で ts-rest を使用したコントローラーの実装例:

import { Controller } from "@nestjs/common";
import {
  TsRestHandler,
  nestControllerContract,
  tsRestHandler,
} from "@ts-rest/nest";
import { apiContract } from "./api-contract";

const restAPIRouter = nestControllerContract(apiContract);

@Controller()
export class UserController {
  constructor(private readonly userService: UserService) {}

  @TsRestHandler(restAPIRouter.getUser)
  async getUser() {
    return tsRestHandler(restAPIRouter.getUser, async ({ params }) => {
      const user = await this.userService.findById(params.id);

      if (!user) {
        return {
          status: 404,
          body: { error: "User not found" },
        };
      }

      return {
        status: 200,
        body: {
          id: user.id,
          name: user.name,
          email: user.email,
        },
      };
    });
  }
}

ts-rest クライアントを使用したフロントエンドの実装例:

import { initClient, apiContract } from "./api-contract";

const apiClient = initClient(apiContract, {
  baseUrl: "https://example.com",
});

const getUser = async (userId: string) => {
  const response = await apiClient.getClient().getUser({
    params: {
      id: userId,
    },
  });

  if (response.status !== 200) {
    throw new Error(`Failed to fetch user: ${response.status}`);
  }

  // 型安全なレスポンス処理
  return response.body;
};

採用した結果

開発効率の向上

Full-Stack TypeScript と ts-rest を採用することで、開発効率と型安全性の両面で大きな効果を実感できました。

型安全性による開発体験の改善

  • コンパイル時エラーの早期発見: API レスポンスの型不整合が開発時に検出される
  • IDE補完の充実: バックエンド・フロントエンド両方で完全
  • な型補完が利用可能になり、開発速度が向上
  • リファクタリングの安全性: API 仕様変更時の影響範囲が明確

チーム開発での効果

  • 新規開発者のオンボーディング: 型定義により理解が容易
  • コードレビューの効率化: 型安全性によりレビュー範囲を絞り込み
  • 技術的負債の削減: 型定義の一元管理により保守性向上
  • API開発効率の向上: 型定義の手作業が減り、開発速度が向上
  • 開発体験の改善: 型関連エラーが開発段階で検出されるようになり、開発効率が向上
  • mock作成の効率化: 型定義の自動生成により、mock 作成が容易になった
  • APIレスポンスの型安全性: 完全に保証されるようになった

AIとの親和性向上

Full-Stack TypeScript を採用したことで AI との親和性が大幅に向上し、存在しないプロパティの参照などが無くなり、以下のような効果も実感できました。

  • コード生成の精度向上: 型定義により、AI がより正確なコードを生成
  • デバッグ支援: AI が型エラーを理解し、適切な修正提案が可能
  • mock生成支援: API 仕様から自動的に mock を生成
  • リファクタリング支援: 型安全性により、AI が安全なリファクタリングを提案
  • エージェントによる自律的な開発: 人が定義した型情報をもとに、AI が適切に実装・デバッグ可能

まとめと今後の展望

主要な成果

Legal Brain エージェントのリリースに向けて、Full-Stack TypeScript を主体とし、ts-rest で型安全性を支えることで、以下の成果を達成しました:

  • 型安全性: バックエンド・フロントエンド間の型不整合を解消
  • 開発効率: API 開発効率が大幅に向上
  • 品質向上: ランタイムエラーを大幅に削減
  • チーム開発: 新規開発者のオンボーディング効率化
  • AI開発支援: ビルド時の型エラーを元に AI が自律的にエラー解消し適切な実装を進めることが可能

今後の展望

この技術スタックにより、Legal Brain エージェントは型安全性を保ちながら、効率的な開発サイクルを実現し、リリースに向けた開発を成功させることができました。

今後も、この技術スタックを基盤として、さらなる開発効率の向上と品質の改善を目指します。特に、チーム内での知識共有やベストプラクティスの確立に力を入れていきたいと思います。また AI の自律的な開発支援をさらに活用し、開発プロセスの自動化や効率化を進めていく予定です。