1. はじめに

1.1 背景

近年のWebアプリケーション開発においては、システムの複雑化に伴い、保守性の担保やDXの向上が重要な課題となっている。特に、フロントエンドとバックエンドを分離したアーキテクチャにおいては、API境界における型の不整合がランタイムエラーの主な要因となる。

そのことから、私は実際に厳格な型システムを持つ言語とバリデーションライブラリを使用する開発を通して、型安全な開発がどのような効果をもたらすのかを検証しようと考えた。

1.2 目的

本研究では、静的型付け言語であるRustとTypeScriptを採用し、チャットアプリケーションの開発を行う。開発プロセスを通じて、型安全なシステムの構築手法を実践し、それが開発効率や保守性に与える影響を考察する。

2. 作成したアプリケーションの概要

本研究で開発したアプリケーションは、リアルタイムな遠隔コミュニケーションを目的としたチャットシステムである。

2.1 ユーザー認証機能

Firebaseを利用したログイン機能である。

脆弱性が生まれやすい認証システムには、外部の認証プロバイダを活用し、セッションと比較して可搬性の高いJWTトークンを利用する。


図1 ログイン画面

2.2 ルーム管理機能

固有のIDを用いたチャットルームの作成・参加機能である。

ユーザーが任意に決められるIDと、自動生成されるIDの選択式にすることで、手軽さとわかりやすさを両立する。ここで作成したIDをURLとして使用することで、ルームへ参加する。


図2 ルーム作成フォーム

2.3 チャット機能

WebSocket通信による低遅延でリアルタイムなメッセージの送受信機能である。

教員と生徒の役割があり、学生と教員は互いにダイレクトメッセージを送ることができる。教員からのメッセージは強調して表示されるようになっている。

これらの機能は、学校の授業への活用のニーズを満たすことができるように設計している。


図3 チャット画面

3. システムアーキテクチャと主要技術

3.1 採用技術の概要

3.1.1 Next.js

Next.jsは、Reactをベースとしたフロントエンド向けフルスタックフレームワークである。ルーティング、レンダリングの最適化、APIルートの構築など、モダンなWebアプリケーションに必要な機能を網羅的に提供している。

本研究では、画面表示に関わるフロントエンドに採用した。しかし、Next.jsの特徴的な機能であるAPIルートは一切使用せず、代わりにデータベース操作を伴うAPIはRustで実装することとした。

3.1.2 RustおよびAxum

Rustは、高い実行パフォーマンスとメモリ安全性を両立したシステムプログラミング言語である。所有権システムによる厳格なメモリ管理を特徴とし、ランタイムエラーの最小化に寄与する。

Axumは、Rustの非同期処理ランタイムである tokio の上で動作するWebアプリケーションフレームワークである。型安全なルーティングや、リクエストとレスポンスを宣言的に扱うハンドラ設計を特徴としており、堅牢なAPIサーバーの構築に適している。

本研究では、これらをデータベースとWebSocket通信に関わるバックエンドに採用した。

3.1.3 Mermaidによる図表

手作業でのUML図記述の工数を削減するため、本研究ではMermaidを使用した。Mermaidはテキストベースで図表を定義できるJavaScriptライブラリである。これにより、Docs as Codeを実践し、記述の一貫性を保つことができる。

以下の2つの図はMermaidによって生成された、本研究のUML図である。Mermaidでは要素の関連のみのトポロジー(抽象的構造)を定義することで、以下のように各図表要素が一意に配置される。

本システムは大きく分けて3つのサーバーで構成されており、フロントエンドのNext.jsサーバー、バックエンドのAxumサーバー、DBのPostgreSQLサーバーが連携して動作している。

特筆すべき点は、Next.jsサーバーはPostgreSQLサーバーと直接通信することがない点である。Next.jsはAPIエンドポイントを持つことが可能だが、サーバー間の境界をまたいだ型定義の連携をすることが本研究の趣旨であるため、フロントエンドとバックエンドを明確に分離した。


図4 アーキテクチャ図

本システムのデータベースはRDBであるPostgreSQLを使用している。バックエンドに使用しているRustは明確なスキーマ定義との相性がよいため、NoSQLよりRDBを採用した。また、Nullを持ちうる値の多用はソースコードを汚染させるため、不要なNullableを避けている。


図5 ER図

3.2 モノレポ構成

フロントエンドとバックエンドのコードは、単一のGitリポジトリで管理するモノレポ方式を採用した。これにより、バージョン管理が容易になるだけでなく、後述するバックエンドからフロントエンドへの型定義のエクスポートをシームレスに実行する基盤となっている。

本システムでは、リポジトリの/(root)下にfrontendとbackendディレクトリを設け、それぞれを異なるプロジェクトとして管理した。

4. JWT(Json Web Token)認証

4.1 Firebase Authentication

本システムでは、Google Firebase Authenticationを利用した認証基盤を構築した。フロントエンドにてGoogleアカウントを用いた認証を行い、JWTを取得する。このJWTをバックエンドAPIの呼び出し時に送信し、APIサーバー側で検証を行う設計とした。

import { auth } from '@/lib/firebase'
 
const login = async () => {
  const provider = new GoogleAuthProvider();
  await signInWithPopup(auth, provider);
};

図6 ログイン時のGoogle認証呼出

4.2 JWTによる認証情報の連携

本システムのバックエンドAPIの認証およびセッション管理には、フロントエンドで生成され、渡されるJWTを使用している。

JWTを利用することで、サーバー側でセッション状態を保持する必要がなくなり、ステートレスな通信が実現する。これは、信頼される認証の提供と、システム境界を跨いだセッション情報の受け渡しにおいて、非常に有用な手段である。

5. 型安全とSSOTの実現

本研究における最も重要な設計方針は、システム全体を貫く型安全性の確保である。

5.1 型境界における課題

本研究のようにフロントエンドとバックエンドが分離したシステムでは、APIの通信仕様を手動で定義・同期する必要があり、この作業工程において人為的ミスが発生しやすい。

5.2 RustとORMを用いたSSOTの構築

この課題を解決するため、本研究ではSSOT(Single Source of Truth)という概念を採用した。SSOTとは、システム内の特定のデータ要素について、その正確性と最新性を保証する唯一の場所を定義する設計原則である。

Webアプリケーションにおいて、フロントエンドとバックエンドで別個に型定義を行うと、仕様変更時に情報の乖離が生じ、重大な不具合を招く。本システムでは、データベースのスキーマ定義をバックエンドRustのORM(Object Relational Mapper)のエンティティとして定義し、これをSSOTとした。

また、各種IDをUUID等のプリミティブ型として直接扱うのではなく、専用の型でラップするNewTypeパターンを導入した。

本システムのこの例では、ユーザーオブジェクトをSeaORMのエンティティとして定義し、主キーをNewTypeでラップしたUUIDとし、それをTypeScriptへエクスポートできるフォーマットにしている。

use sea_orm::entity::prelude::*;
use ts_rs::TS;
 
#[derive(DeriveValueType, TS)]
#[ts(export)]
pub struct UserId(pub uuid::Uuid);
 
#[derive(DeriveEntityModel, TS)]
#[sea_orm(table_name = "users")]
#[ts(export)]
pub struct Model {
  #[sea_orm(primary_key)]
  pub id: UserId, 
  pub email: String,
}

図7 NewTypeパターンとSeaORMによるSSOTの定義

5.3 フロントエンドへの型の伝播

バックエンドで定義された型情報はRustのts-rsライブラリを用いて、TypeScriptの型定義ファイルをフロントエンドのフォルダに自動出力する。フロントエンドの実装ではこの生成された型定義を原則的に使用する。

この際、TypeScript側ではBranded Typesを用いることで、名目的型付けを実現した。TypeScriptはデフォルトで構造的型付けを採用しているため、構造が同じであれば異なるIDでも代入できてしまうが、Branded Typesによりこれを阻止し、バックエンドの厳格な制約をフロントエンドへ継承させている。

export type UserId = string & { __brand: "UserId" };
export type RoomId = string & { __brand: "RoomId" };
const userId: UserId = "019cad8f…" as UserId;
 
// Error: Type 'UserId' is not assignable to type 'RoomId'
const roomId: RoomId = userId;

図8 Branded Typesによる名目的型付け

さらに、流入する不確実なデータを検証するためにバリデーションライブラリであるZodを導入した。外部からのリクエスト内のデータは必ずスキーマ検証を通過させ、成功したもののみをアプリケーション内部へ受け入れる。

import { z } from "zod";
 
const UserIdSchema = z.string()
  .uuid()
  .transform(val => val as UserId);
const rawData: unknown = await fetch("/api/user")
  .then(r => r.json());
 
// 検証失敗時はここでランタイムエラー
const userId = UserIdSchema.parse(rawData);

図9 Zodによるバリデーション

結果として、API仕様の変更による不整合は即座にフロントエンドのコンパイルエラーとして検知されるようになった。

5.4 活用例

ルーム作成機能を例にとり、この型システムがどのように機能しているのか概説する。

フロントエンドのからルーム名とルームIDをひとまとめにしたリクエストペイロードを作成する。このリクエストペイロードの型定義はRustで定義されたstructをts-rsでエクスポートされたものを使用する。

バックエンドでも同一のリクエストペイロードの型定義を使用してリクエストを受信する。その後、作成したルームをSeaORMのエンティティの型として扱い、フロントエンドにレスポンスを返す。

最終的にフロントエンドでは、ルーム情報を含むレスポンスをts-rsで生成されたSeaORMのエンティティの型として受け取ることにより、全く同じ型定義を使用するSSOTが実現出来ている。

5.5 型情報伝播のフロー

以下の図はMermaidで生成された、チャットアプリケーション内の型定義がサーバー間でどのように伝播し、いつ型検査が行われているかを表したフローチャートである。

特筆すべきは、ts-rs(図中のType Boundary内)が異なる言語間での型定義の連携を担っている点である。ts-rsがRustのSeaORMのエンティティの型定義(図中のBackend Layer内SeaORM Entities)をTypeScriptの型の形式に変換し、フロントエンド内のプロジェクトにエクスポートすることで、SSOTが保たれている。


図10 フローチャート

6. 考察

6.1 モダン技術がもたらしたDXと保守性の評価

本アーキテクチャの導入により、コンパイルエラーの解決には一定のコストを要した。しかし、「コンパイルが成功すれば、型に起因するランタイムエラーは発生しない」という状態が担保されたことで、結果的にデバッグの時間が大幅に削減され、DXの向上に寄与したと評価できる。

また、今回は個人開発だったが、大規模開発でこそこのアプローチは効果を大きく発揮する。型が間違っている際にランタイムエラーでなくコンパイルエラーを極力出すことで、予期せぬ型定義のミスを大きく減らすことにより、バグ密度を減らすことに寄与できる。

6.2 Next.js採用のデメリット

一方で、本アプリケーションにおけるフロントエンドフレームワークとしてのNext.jsの採用については、必ずしも適切ではなかったと考察する。その理由は以下のとおりである。

  1. Next.jsは本来フルスタックフレームワークであり、フロントエンドとバックエンドを単一のプロジェクトで管理できるが、本プロジェクトのアーキテクチャではフロントエンドとバックエンドを明確に分離しているため、その利点を活かしていない。むしろ、APIをフロントエンドとバックエンドのどちらに書くかの線引きを難しくする要因となる。

  2. Next.jsの主要な機能のひとつにHTMLの静的生成をし、初期表示時の遅延を減らすことで、SEO(Search Engine Optimization:検索エンジン最適化)効果をもたらすSSG(Static Site Generation:静的サイト生成)/SSR(Server Side Rendering)があるが、チャットアプリケーションの性質上、画面遷移は少なく、SPAの性質が強いため、その機能を存分に活かすことはできない。また、学校内の閉じたアプリケーションであるため、SEOを考慮する必要性はない。

  3. Next.jsはReactよりも多くの機能をもっており、それはビルド時間を増加させる。また、学習コストを増大させる原因にもなる。

  4. Next.jsは静的に生成したHTMLからReactで動的にHTMLを生成する橋渡しをするHydrationという機能があり、Hydration時に整合性を確認できなかった場合にエラーを発生させる。このHydrationの対応による、DXの低下を引き起こす場合がある。

すなわち、Next.jsの利点を最大限活かすことができないのなら、フロントエンドにはReactのみを使用することを検討するべきである。

だが、それでもApp Routerなどの機能は非常に有用であるため、必ずしもNext.jsを使用することが誤りにはならない。

6.3 複数言語を採用するコスト

単一のプロジェクトで複数の言語を使用して開発することは学習コストの低下を招く。特に、Rustは新規学習のコストが高いと言われているため、より顕著である。

7. おわりに

本研究では、モダンな技術スタックを用いたチャットアプリケーションの開発を通じ、SSOTに基づく型安全なシステム構築を実践した。静的型検査とコード生成を組み合わせた開発手法は、ソフトウェアの保守性および開発者体験を飛躍的に向上させる有効なアプローチであることが確認できた。

8. 参考文献および参考サイト