0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FrontendとBackendの境界を、モダンなTypeScriptでどこまで綺麗に作れるか

0
Posted at

はじめに

TanStack Start の Server Function と Hono の HTTP API を、同じ Service / Repository の上に載せて比較した実験記録と、そこから導いた「境界」についての考察。

この記事では、各節で次のラベルを使い、事実と考察を分けて書く。

ラベル 意味
[Observed] 実験で実際に確認したこと
[Interpretation] その結果をどう解釈できるか
[Implication] 設計上、何を考える材料になるか
[Not Verified] 今回の実験では確認していないこと

「TanStack Start と Hono のどちらを使うべきか」を決める記事ではない。どちらが優れているか、どちらが速いかという結論も出さない。

扱う問いは次の一点である。

FrontendとBackendの境界を、現代のTypeScriptではどのように設計できるのか

この問いを、最小の Developer Portal を実装して 2 つの経路で同じユースケースを実現し、Jupyter Notebook で観測した結果から考察する。


なぜこの実験をするのか

従来の Frontend / Backend 開発では、境界は比較的はっきりしていた。

Frontend と Backend の間には HTTP API があり、境界は「HTTP であること」とほぼ同義だった。

一方、TanStack Start のような Full-stack TypeScript framework では、Server Function によって、Server 側の処理を Frontend のコードからかなり近い形で呼び出せる。対して Hono は、HTTP API という境界を明示的に設計する側に立つ。

そこで次の疑問が出てきた。

この2つは、実際には何が違うのか?

「関数呼び出しのように書ける」と「HTTP API として設計する」の違いを、印象ではなく、同じ Service / Repository を共有した状態で観測して確かめる。これが実験の動機である。


作ったもの

技術構成

TypeScript / React / TanStack Start / TanStack Router / Hono / PostgreSQL / Docker / Jupyter Notebook

ドメイン

Project と Event だけを持つ、非常に小さな Developer Portal である。

Project: id, name, status, createdAt
Event:   id, projectId, type, createdAt

UI

/projects              Project一覧
/projects/:projectId   Project詳細(関連Eventも表示)

Hono API

GET  /api/projects
GET  /api/projects/:projectId
POST /api/projects/:projectId/events

ドメインを小さくしたのは意図的である。ビジネスロジックの複雑さを排除し、比較対象を「Backend の処理」ではなく「Backend へ到達する入口」に絞るためである。


アーキテクチャ

責務は次のように分離している。

backend/
├── model/
├── controller/
├── service/
└── repository/
層 責務
Model ドメインデータの型・構造を表現する
Repository PostgreSQL とのデータアクセスを担当する
Service Project 取得や Event 登録などのユースケースを担当する
Controller HTTP API における HTTP リクエスト / レスポンスの責務を担当する

重要なのは、Server Function と Hono Controller の両方が同じ Service を利用する点である。

この設計にした理由

比較したいのは Backend のビジネスロジックではなく、「Frontend / Client から Backend の処理へ到達する境界」である。Service と Repository を共通化すれば、2 経路の差は入口に限定される。入口以外の変数を固定することで、観測された差を入口に帰属させやすくなる。

また Controller の責務を「HTTP のリクエスト / レスポンス」に限定したことで、HTTP 固有の関心事と、ユースケースとしての責務が、コード上で分かれる。この分離は後述する「責務上の境界」の議論の前提になる。


Server FunctionとHono

Path A: TanStack Start Server Function

Path B: Hono HTTP API

コードが表現する境界

以下は、境界の表現を示すための最小例である(実装の抜粋ではなく、構造を示す簡略版)。

Server Function → Service

export const getProjects = createServerFn({ method: "GET" }).handler(
  async () => projectService.listProjects(),
);

この形では、Frontend のコードから見た境界は「関数の呼び出し」として表現される。引数と戻り値の型は呼び出し側のコードから直接見える。HTTP のパスやメソッドを呼び出し側が組み立てる場面は、コード上には現れない。

Hono Controller → Service

app.get("/api/projects", async (c) => {
  const projects = await projectService.listProjects();
  return c.json(projects);
});

この形では、境界は「HTTP リクエストとレスポンス」として表現される。パス、メソッド、ステータス、シリアライズ形式が、契約としてコードに明示される。

共通 Service → Repository

export const projectService = {
  listProjects: () => projectRepository.findAll(),
};

Service は入口を知らない。HTTP も Server Function も、この層には現れない。この層が、2 つの入口の責務上の境界を担っている。


実験方法

notebooks/architecture-comparison.ipynb で、同じ操作を Server Function と Hono API の両方から実行し、結果と通信の様子を観測した。


実験結果

Project取得

[Observed]
Server Function と Hono API の両方から Project を取得でき、両者で取得したデータ内容は一致した。

[Interpretation]
2 つの入口が、同じ Service を経由して同じ種類のデータを返していることと整合する。

Event登録

[Observed]

  • Server Function から Event を登録できた。
  • Hono API から Event を登録できた。
  • Server Function から登録した Event を Hono API から取得できた。
  • Hono API から登録した Event を Server Function から取得できた。

データ整合性

[Observed]
上記の相互取得が成立した。

[Interpretation]
片方の入口から書き込んだデータを、もう片方の入口から読めたことから、両経路が同じ Service / Repository / PostgreSQL を利用していることを確認できた。

[Not Verified]
同時書き込み、トランザクション境界、競合時の挙動は確認していない。

通信境界

[Observed]

呼び出し元 呼び出し先 観測内容
Notebook Server Function Node adapter ではプロセス内で呼び出されており、アプリケーションの HTTP route にはアクセスしなかった
Browser Server Function ローカル開発環境で GET /_serverFn/... という fetch を観測した
Notebook Hono GET /api/projects などの HTTP API として呼び出した

Browser から観測した GET /_serverFn/... は、今回観測した GET リクエストについての結果である。Server Function 一般のすべての通信方式についての結果ではない。

[Interpretation]
同じ Server Function でも、呼び出し元によって到達の仕方が異なっていた。Hono は、どこから呼んでも HTTP API として到達する。Backend の処理への到達方法と、通信境界の見え方は、入口によって異なる。

[Implication]
「Backend へ到達する」ことと「HTTP を話す」ことは、設計上、切り分けて考えられる。

[Not Verified]
POST 等の他メソッド、他の adapter、本番ビルド、他のデプロイ形態での通信方式は確認していない。

応答時間について

応答時間は測定したが、Server Function と Hono の性能比較には使えない。測定条件が一致していないためである。

Server Function 側: 起動等(Node / Vite / アプリケーション起動)+ 処理
Hono 側:            すでに起動している API への HTTP 往復 + 処理

したがって、この測定値から速度・性能の優劣を述べることはしない。性能比較のグラフやランキングも作らない。


結果から考えるFrontend / Backendの境界

BackendはHTTP APIなのか

[Interpretation]
従来は Frontend → HTTP → Backend を Backend の基本形と考えることが多かった。今回、Server Function は Frontend のコードから関数に近い形で呼び出せた。Backend という論理的な責務と、HTTP という通信プロトコルは、必ずしも同一ではない。

[Implication]
Backend を設計するとき、「責務をどこに置くか」と「どのプロトコルで到達させるか」は別の判断として扱える。

[Not Verified]
この解釈は、今回の最小構成の観測に基づく。大規模アプリケーションで同じ整理が有効かは確認していない。

Server FunctionはBackendをなくすのか

[Observed]
今回の構成でも、Server Function の先には Service、Repository、PostgreSQL があった。

[Interpretation]
Server Function によって Backend が消えたわけではない。Server 側の責務は存在し続けている。変わったのは、Frontend から Server 側の処理へ到達する際の境界の表現である。

HTTP境界を明示する意味

Hono は、Client → HTTP → Hono → Service という明示的な HTTP 境界を設計できる。

[Interpretation]
外部 Client、Webhook、他サービス、別 Frontend、Public API のように、契約を HTTP として明示したい相手がいる場合、パス・メソッド・ステータス・ペイロードという形で境界が文書化可能になる。

「HTTP 境界が必要なら Hono を使う」という話ではない。HTTP を境界として明示すること自体が、アーキテクチャ上の意味を持つ。契約が明示されれば、利用者は実装の内部を知らずに連携できる。一方、境界を明示すれば、その契約を維持するコストも発生する。

[Not Verified]
外部公開 API としての運用(バージョニング、互換性維持、認証)は確認していない。

TypeScriptで境界をどう表現するか

「境界」は一つではない。今回の題材から、次のように整理できる。

境界 今回の実験で
Type Boundary 未検証(型がどこまで伝播するかは今後の課題)
Communication Boundary 観測した(プロセス内呼び出し / fetch / HTTP)
Process Boundary 一部観測した(Notebook からの呼び出しがプロセス内だったこと)
HTTP Boundary 観測した(Hono は常に HTTP、Server Function は呼び出し元で異なる)
Responsibility Boundary 構造として確認した(Service が共通で、入口と責務が分離)
Deployment Boundary 未検証

また、次の3つは別物である。

  • コード上の境界: 関数呼び出しに見えるか、HTTP ハンドラに見えるか
  • 通信上の境界: 実際にネットワークやプロセスをまたぐか
  • 責務上の境界: どの層が何に責任を持つか

Server Function は、コード上は関数呼び出しに近いが、Browser からは fetch として観測された。コード上の見え方と通信上の見え方は一致しない。そして責務上の境界は、どちらの入口でも Service 層が担っていた。

[Implication]
境界を議論するときは、どの種類の境界について話しているのかを明示する必要がある。「境界が曖昧になる」「境界がなくなる」という表現は、どの境界のことなのかが示されて初めて検証可能になる。


TanStack Start × Honoを組み合わせる意味

UI 内部の処理、外部 Client との通信、HTTP API、Webhook は、境界の性質が異なる。

[Interpretation]
境界の性質が違うなら、入口の技術を一つに揃える必然性はない。入口ごとに適した表現を選び、ユースケースは同じ Service 層に収束させる。今回の実験は、この構造が成立すること(両入口からの相互読み書き)を示している。

[Implication]
境界の性質によって、適切な技術を選択できる。ただし、これは「この構成が常に正解」という主張ではない。入口が増えれば、認証、エラー表現、デプロイ、運用の面で別のコストが生じうる。

[Not Verified]
組み合わせた構成の運用コスト、認証の統一、エラー表現の一貫性は確認していない。


今回分からなかったこと

今回の実験だけでは、次の項目は判断できない。推測で結論は出さない。

  • 本番環境での性能
  • 高負荷時の挙動
  • Server Function と Hono の性能差
  • 大規模アプリケーションでの保守性
  • 認証境界
  • 外部公開 API としての運用
  • エラー処理の複雑性
  • チーム開発時の境界設計
  • デプロイ境界
  • Server Function と Hono を組み合わせた場合の運用コスト

特に、実験環境(ローカル開発、最小ドメイン、Notebook からの呼び出し)で確認できたことと、フレームワーク一般の特性は区別して読んでほしい。


次に試したいこと

優先度は付けない。

  • Error Boundary: Server Function と Hono API で、エラーをどこまで自然に表現できるか
  • Type Boundary: TypeScript の型がどこまで共有・伝播するか
  • Authentication Boundary: 認証・認可をどこに置くのが自然か
  • External Client: Hono API を外部 Client から利用した場合の設計
  • Webhook: Hono の HTTP 境界が Webhook 受信にどう適合するか
  • Deployment Boundary: Server Function と Hono を本番環境へデプロイした場合の構成

まとめ

今回の実験で重要だったのは、Server Function と Hono のどちらが優れているかを決めることではない。

同じ Service / Repository を使いながら異なる入口を作ったことで、次のことを確認できた。

FrontendからBackendへ処理が渡るとき、「責務の境界」と「通信の境界」は必ずしも同じではない

そして、モダンな TypeScript では、Frontend / Backend の境界を「HTTP API かどうか」だけでなく、責務・型・通信・プロセスといった複数の観点から設計できる。

この記事で確認できたのは最小構成での観測にすぎず、型・認証・デプロイなどは今後の検証対象である。それでも、「どの境界の話をしているのか」を分けて考える視点自体は、今後の設計判断の材料になると考えている。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?