T-CREATOR

htmx で実現するハイパーメディア設計:リンクとフォームを API として扱う

htmx で実現するハイパーメディア設計:リンクとフォームを API として扱う

Web アプリケーション開発において、API 設計は常に重要なテーマです。近年、SPA(Single Page Application)が主流となり、JSON を返す REST API が一般的になりました。しかし、htmx を活用することで、HTML そのものを API レスポンスとして扱う「ハイパーメディア駆動」の設計が再び注目を集めています。

この記事では、htmx を使って「リンクとフォームを API として扱う」ハイパーメディア設計の考え方と実装方法を、初心者の方にもわかりやすく解説します。従来の JSON API とは異なるアプローチで、シンプルかつ強力な Web アプリケーションを構築できるようになりますよ。

背景

ハイパーメディアとは何か

ハイパーメディアとは、リンクや埋め込みリソースを含むメディアのことを指します。Web における最も代表的なハイパーメディアは HTML です。

HTML は単なるデータ表現ではなく、<a> タグや <form> タグによって「次に何ができるか」という情報を含んでいます。この特性により、ブラウザはサーバーとの対話方法を動的に理解できるのです。

以下の図は、ハイパーメディアの基本的な構造を示しています。

mermaidflowchart TB
  client["クライアント<br/>(ブラウザ)"]
  server["サーバー"]
  html["HTML レスポンス"]
  links["リンク情報<br/>(次のアクション)"]

  client -->|リクエスト| server
  server -->|返却| html
  html -->|含む| links
  links -->|提示| client

この図から、HTML が単なる表示データではなく、次のアクションへの道筋を示す「ナビゲーション情報」を含んでいることがわかります。

REST と HATEOAS の関係

REST(Representational State Transfer)の提唱者である Roy Fielding は、REST の重要な制約の一つとして「HATEOAS(Hypermedia As The Engine Of Application State)」を定義しました。

HATEOAS とは、アプリケーションの状態遷移がハイパーメディアによって駆動されるべきという考え方です。つまり、クライアントは API のエンドポイント URL をハードコードするのではなく、サーバーから返されるハイパーメディア(リンクやフォーム)を辿ることで操作を進めるべきなのです。

しかし、多くの REST API は JSON を返すだけで、ハイパーメディアの特性を活用していません。これは厳密には「REST」ではなく「HTTP API」と呼ぶべきものです。

mermaidsequenceDiagram
  participant Client as クライアント
  participant Server as サーバー

  Client->>Server: GET /users/123
  Server->>Client: JSON データ + リンク情報
  Note over Client: リンク情報から次の<br/>アクションを判断
  Client->>Server: DELETE /users/123<br/>(リンクから取得)
  Server->>Client: 204 No Content

上記のシーケンス図は、HATEOAS に従った API の動作を示しています。クライアントは URL をハードコードせず、サーバーから提供されるリンク情報を使って次のアクションを決定します。

課題

従来の SPA アプローチの問題点

現代の Web 開発では、React や Vue.js などのフレームワークを使った SPA が一般的です。しかし、このアプローチにはいくつかの課題があります。

複雑性の増加

SPA では、フロントエンドとバックエンドを完全に分離します。そのため、以下のような作業が必要になります。

  • JSON API の設計と実装
  • フロントエンド側での状態管理
  • API クライアントコードの作成
  • データの型定義の二重管理
  • エラーハンドリングの実装

これらの作業により、開発の複雑性が大幅に増加してしまいます。

エンドポイントのハードコーディング

多くの SPA では、API エンドポイントをフロントエンドコードに直接記述します。

typescript// フロントエンド側のコード例
const API_ENDPOINTS = {
  getUser: (id: string) => `/api/users/${id}`,
  updateUser: (id: string) => `/api/users/${id}`,
  deleteUser: (id: string) => `/api/users/${id}`,
};

// コンポーネント内での利用
const response = await fetch(API_ENDPOINTS.getUser('123'));

上記のコードは、API エンドポイントをフロントエンドにハードコードしています。この方法では、API の URL 構造が変更されるたびにフロントエンドコードも修正が必要です。

結合度の高さ

エンドポイントをハードコードすることで、フロントエンドとバックエンドの結合度が高くなります。これにより、以下の問題が発生します。

#問題点影響
1URL 変更時の修正コストフロントエンドとバックエンドの両方を修正
2API バージョニングの困難クライアント側の大規模な書き換えが必要
3テストの複雑化モックの作成とメンテナンスが煩雑
4ドキュメント管理API 仕様書とコードの同期が困難

以下の図は、従来の SPA アプローチにおける結合の問題を示しています。

mermaidflowchart LR
  subgraph Frontend["フロントエンド"]
    comp["コンポーネント"]
    urls["ハードコードされた<br/>URL定義"]
  end

  subgraph Backend["バックエンド"]
    api["REST API<br/>(JSON)"]
  end

  comp -.->|密結合| urls
  urls -->|固定URL| api

  style urls fill:#ff6b6b

この図から、フロントエンドが URL 定義に強く依存している状態が見てとれます。URL の変更がフロントエンドに直接影響を与えてしまうのです。

JSON レスポンスの限界

JSON API は構造化されたデータを返しますが、「次に何ができるか」という情報は含まれていません。

json{
  "id": 123,
  "name": "山田太郎",
  "email": "yamada@example.com"
}

上記の JSON レスポンスは、ユーザー情報のデータのみを返しています。このユーザーを編集・削除できるのか、どの URL にアクセスすればよいのかといった情報は含まれていません。

クライアント側は、これらの操作方法を事前に知っている必要があります。つまり、API の仕様をハードコードしなければならないのです。

解決策

htmx によるハイパーメディア駆動設計

htmx は、HTML を拡張することで、ハイパーメディアの力を最大限に引き出すライブラリです。htmx を使うことで、リンクとフォームそのものが「API の使い方」を表現できるようになります。

基本的な考え方

htmx では、サーバーは JSON ではなく HTML フラグメントを返します。この HTML には、次のアクションを示すリンクやフォームが含まれています。

html<!-- サーバーから返される HTML レスポンス -->
<div id="user-card">
  <h3>山田太郎</h3>
  <p>yamada@example.com</p>

  <!-- 編集アクション -->
  <button hx-get="/users/123/edit" hx-target="#user-card">
    編集
  </button>

  <!-- 削除アクション -->
  <button
    hx-delete="/users/123"
    hx-confirm="本当に削除しますか?"
    hx-target="#user-card"
  >
    削除
  </button>
</div>

上記のコードでは、HTML 自体が「このユーザーは編集と削除ができる」という情報を含んでいます。クライアント側のコードは、これらの URL を知る必要がありません。

以下の図は、htmx を使ったハイパーメディア駆動設計のフローを示しています。

mermaidflowchart TB
  browser["ブラウザ"]
  htmx["htmx"]
  server["サーバー"]
  html_resp["HTML レスポンス<br/>(アクション情報含む)"]

  browser -->|初回アクセス| server
  server -->|HTML返却| browser
  browser -.->|イベント検知| htmx
  htmx -->|AJAX リクエスト| server
  server -->|部分HTML| html_resp
  html_resp -->|DOM更新| browser

  style html_resp fill:#4ecdc4

この図から、htmx がブラウザとサーバー間の橋渡しをし、HTML ベースの動的な対話を実現していることがわかります。

リンクを API エンドポイントとして扱う

htmx では、<a> タグや <button> タグに hx-gethx-posthx-delete などの属性を追加することで、そのリンクが API エンドポイントとして機能します。

GET リクエストの例

html<!-- ユーザー詳細を取得するリンク -->
<a
  href="/users/123"
  hx-get="/users/123"
  hx-target="#content"
  hx-swap="innerHTML"
>
  ユーザー詳細を表示
</a>

上記のコードでは、hx-get 属性によって、クリック時に ​/​users​/​123 へ AJAX リクエストが送信されます。レスポンスの HTML は #content 要素の内部に挿入されます。

htmx 属性の説明

#属性名役割
1hx-getGET リクエストを送信hx-get="​/​users​/​123"
2hx-targetレスポンスを挿入する要素hx-target="#content"
3hx-swap挿入方法の指定hx-swap="innerHTML"
4hx-triggerイベントトリガーの指定hx-trigger="click"

DELETE リクエストの例

html<!-- ユーザーを削除するボタン -->
<button
  hx-delete="/users/123"
  hx-confirm="本当に削除しますか?"
  hx-target="#user-123"
  hx-swap="outerHTML"
>
  削除
</button>

上記のコードでは、hx-delete 属性によって DELETE リクエストが送信されます。hx-confirm によって確認ダイアログが表示され、ユーザーの誤操作を防ぎます。

フォームを API エンドポイントとして扱う

htmx を使うことで、フォームも強力な API インターフェースとして機能します。

基本的なフォームの例

html<!-- ユーザー作成フォーム -->
<form
  hx-post="/users"
  hx-target="#user-list"
  hx-swap="beforeend"
>
  <label for="name">名前:</label>
  <input type="text" id="name" name="name" required />

  <label for="email">メールアドレス:</label>
  <input type="email" id="email" name="email" required />

  <button type="submit">作成</button>
</form>

上記のフォームは、送信時に ​/​users へ POST リクエストを送信します。レスポンスの HTML は #user-list の末尾に追加されます。

フォームのバリデーション

htmx は HTML5 のフォームバリデーションと自然に統合されます。

html<!-- バリデーション付きフォーム -->
<form hx-post="/users" hx-target="#result">
  <input
    type="text"
    name="username"
    minlength="3"
    maxlength="20"
    required
    pattern="[a-zA-Z0-9_]+"
  />

  <input type="email" name="email" required />

  <button type="submit">登録</button>
</form>

上記のコードでは、HTML5 のバリデーション属性(requiredpattern など)によって、クライアント側でのバリデーションが自動的に行われます。

サーバー側の実装

サーバー側では、HTML フラグメントを返すエンドポイントを実装します。以下は Node.js と Express を使った例です。

ユーザー詳細を返すエンドポイント

javascript// Express のルーティング設定
const express = require('express');
const app = express();

// ユーザー詳細を返すエンドポイント
app.get('/users/:id', async (req, res) => {
  const userId = req.params.id;

  // データベースからユーザー情報を取得
  const user = await db.getUser(userId);

  // HTML フラグメントを返す
  res.send(`
    <div id="user-${user.id}">
      <h3>${user.name}</h3>
      <p>${user.email}</p>

      <!-- 編集と削除のアクションを含む -->
      <button hx-get="/users/${user.id}/edit"
              hx-target="#user-${user.id}">
        編集
      </button>

      <button hx-delete="/users/${user.id}"
              hx-target="#user-${user.id}"
              hx-swap="outerHTML">
        削除
      </button>
    </div>
  `);
});

上記のコードでは、ユーザー情報だけでなく、そのユーザーに対して可能なアクション(編集・削除)も HTML として返しています。

ユーザー作成エンドポイント

javascript// ユーザー作成を処理するエンドポイント
app.post('/users', async (req, res) => {
  // フォームデータを取得
  const { name, email } = req.body;

  // バリデーション
  if (!name || !email) {
    return res.status(400).send(`
      <div class="error">
        名前とメールアドレスは必須です
      </div>
    `);
  }

  // ユーザーを作成
  const newUser = await db.createUser({ name, email });

  // 新しいユーザーカードを返す
  res.send(`
    <div id="user-${newUser.id}" class="user-card">
      <h3>${newUser.name}</h3>
      <p>${newUser.email}</p>

      <button hx-get="/users/${newUser.id}/edit"
              hx-target="#user-${newUser.id}">
        編集
      </button>

      <button hx-delete="/users/${newUser.id}"
              hx-target="#user-${newUser.id}"
              hx-swap="outerHTML">
        削除
      </button>
    </div>
  `);
});

上記のコードでは、作成されたユーザーの HTML カードを返します。このカードには編集・削除のアクションが含まれているため、クライアント側は何も知らなくても操作できます。

削除エンドポイント

javascript// ユーザー削除を処理するエンドポイント
app.delete('/users/:id', async (req, res) => {
  const userId = req.params.id;

  // ユーザーを削除
  await db.deleteUser(userId);

  // 空のレスポンスを返す
  // (hx-swap="outerHTML" により要素が削除される)
  res.send('');
});

上記のコードでは、削除後に空の HTML を返します。htmx の hx-swap="outerHTML" と組み合わせることで、要素が DOM から削除されます。

以下の図は、サーバー側の処理フローを示しています。

mermaidflowchart TB
  request["htmx リクエスト"]
  route["ルーティング"]
  logic["ビジネスロジック"]
  template["HTML 生成"]
  actions["アクション情報<br/>を含む"]
  response["HTML レスポンス"]

  request --> route
  route --> logic
  logic --> template
  template --> actions
  actions --> response

  style actions fill:#ffd93d

この図から、サーバーが HTML を生成する際に、次のアクション情報も含めて返していることがわかります。

具体例

CRUD 操作の完全な実装

ここでは、ユーザー管理機能の CRUD(作成・読取・更新・削除)操作を htmx で実装する完全な例を示します。

HTML ページの基本構造

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>ユーザー管理</title>
    <!-- htmx を読み込み -->
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
  </head>
  <body>
    <h1>ユーザー管理システム</h1>

    <!-- ユーザー作成フォーム -->
    <section id="create-user">
      <h2>新規ユーザー作成</h2>
      <!-- ここにフォームが入る -->
    </section>

    <!-- ユーザー一覧 -->
    <section id="user-list">
      <h2>ユーザー一覧</h2>
      <!-- ここにユーザーカードが動的に追加される -->
    </section>
  </body>
</html>

上記のコードは、ユーザー管理システムの基本的な HTML 構造です。htmx のスクリプトを読み込むだけで、特別な設定は不要です。

ユーザー作成フォーム

html<!-- 新規ユーザー作成フォーム -->
<form
  hx-post="/users"
  hx-target="#user-list"
  hx-swap="afterbegin"
  hx-on::after-request="this.reset()"
>
  <div class="form-group">
    <label for="name">名前:</label>
    <input
      type="text"
      id="name"
      name="name"
      required
      minlength="2"
    />
  </div>

  <div class="form-group">
    <label for="email">メールアドレス:</label>
    <input type="email" id="email" name="email" required />
  </div>

  <button type="submit">作成</button>
</form>

上記のフォームでは、hx-on::after-request="this.reset()" によって、送信成功後にフォームが自動的にリセットされます。これにより、連続してユーザーを作成できます。

ユーザーカードの表示

サーバーから返される各ユーザーの HTML カードは以下のような構造です。

html<!-- ユーザーカードテンプレート -->
<div id="user-${id}" class="user-card">
  <!-- ヘッダー部分 -->
  <div class="user-header">
    <h3>${name}</h3>
    <span class="user-id">#${id}</span>
  </div>

  <!-- 情報表示部分 -->
  <div class="user-info">
    <p><strong>メール:</strong> ${email}</p>
    <p><strong>登録日:</strong> ${createdAt}</p>
  </div>

  <!-- アクションボタン -->
  <div class="user-actions">
    <button
      hx-get="/users/${id}/edit"
      hx-target="#user-${id}"
      hx-swap="outerHTML"
    >
      編集
    </button>

    <button
      hx-delete="/users/${id}"
      hx-confirm="「${name}」を削除しますか?"
      hx-target="#user-${id}"
      hx-swap="outerHTML"
    >
      削除
    </button>
  </div>
</div>

上記のカードには、ユーザー情報と共に「編集」「削除」のアクションボタンが含まれています。これらのボタンは、そのユーザーに対する API エンドポイントを表現しています。

編集フォームの表示

編集ボタンをクリックすると、ユーザーカードが編集フォームに置き換わります。

html<!-- 編集フォーム -->
<form
  id="user-${id}"
  class="user-edit-form"
  hx-put="/users/${id}"
  hx-target="#user-${id}"
  hx-swap="outerHTML"
>
  <div class="form-group">
    <label for="edit-name-${id}">名前:</label>
    <input
      type="text"
      id="edit-name-${id}"
      name="name"
      value="${name}"
      required
    />
  </div>

  <div class="form-group">
    <label for="edit-email-${id}">メールアドレス:</label>
    <input
      type="email"
      id="edit-email-${id}"
      name="email"
      value="${email}"
      required
    />
  </div>

  <div class="form-actions">
    <button type="submit">保存</button>

    <button
      type="button"
      hx-get="/users/${id}"
      hx-target="#user-${id}"
      hx-swap="outerHTML"
    >
      キャンセル
    </button>
  </div>
</form>

上記のフォームでは、hx-put 属性によって PUT リクエストが送信されます。キャンセルボタンは、元のユーザーカード表示に戻すための GET リクエストを送信します。

サーバー側の完全な実装

以下は、Express と TypeScript を使ったサーバー側の完全な実装例です。

型定義

typescript// types.ts - ユーザーの型定義
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// リクエストボディの型
interface CreateUserRequest {
  name: string;
  email: string;
}

interface UpdateUserRequest {
  name?: string;
  email?: string;
}

上記の型定義により、TypeScript の型安全性を活用できます。

ルーティングとコントローラー

typescript// userRoutes.ts - ユーザー関連のルーティング
import express from 'express';
import { UserController } from './userController';

const router = express.Router();
const controller = new UserController();

// ユーザー一覧取得
router.get('/users', controller.list);

// ユーザー詳細取得
router.get('/users/:id', controller.show);

// ユーザー作成
router.post('/users', controller.create);

// ユーザー編集フォーム取得
router.get('/users/:id/edit', controller.edit);

// ユーザー更新
router.put('/users/:id', controller.update);

// ユーザー削除
router.delete('/users/:id', controller.delete);

export default router;

上記のコードでは、REST の原則に従ったルーティングを定義しています。注目すべきは ​/​users​/​:id​/​edit というエンドポイントで、これは編集フォームを返すための専用エンドポイントです。

コントローラーの実装(一覧と詳細)

typescript// userController.ts - コントローラーの実装
import { Request, Response } from 'express';
import { UserService } from './userService';
import {
  renderUserCard,
  renderUserList,
} from './templates';

export class UserController {
  private service = new UserService();

  // ユーザー一覧を返す
  list = async (req: Request, res: Response) => {
    try {
      // データベースから全ユーザーを取得
      const users = await this.service.findAll();

      // HTML を生成して返す
      const html = renderUserList(users);
      res.send(html);
    } catch (error) {
      res
        .status(500)
        .send(
          '<div class="error">エラーが発生しました</div>'
        );
    }
  };

  // 特定ユーザーの詳細を返す
  show = async (req: Request, res: Response) => {
    try {
      const { id } = req.params;
      const user = await this.service.findById(id);

      if (!user) {
        return res
          .status(404)
          .send(
            '<div class="error">ユーザーが見つかりません</div>'
          );
      }

      // ユーザーカードの HTML を生成
      const html = renderUserCard(user);
      res.send(html);
    } catch (error) {
      res
        .status(500)
        .send(
          '<div class="error">エラーが発生しました</div>'
        );
    }
  };
}

上記のコードでは、各アクションが HTML フラグメントを返しています。エラー時も HTML でエラーメッセージを返すことで、一貫性のあるレスポンスを実現しています。

コントローラーの実装(作成と更新)

typescript// userController.ts の続き
export class UserController {
  // ... 前のコードの続き

  // 新規ユーザーを作成
  create = async (req: Request, res: Response) => {
    try {
      const { name, email } = req.body as CreateUserRequest;

      // バリデーション
      if (!name || !email) {
        return res.status(400).send(`
          <div class="error">
            名前とメールアドレスは必須です
          </div>
        `);
      }

      // メールアドレスの重複チェック
      const exists = await this.service.existsByEmail(
        email
      );
      if (exists) {
        return res.status(400).send(`
          <div class="error">
            このメールアドレスは既に使用されています
          </div>
        `);
      }

      // ユーザーを作成
      const user = await this.service.create({
        name,
        email,
      });

      // 新しいユーザーカードを返す
      const html = renderUserCard(user);
      res.status(201).send(html);
    } catch (error) {
      res
        .status(500)
        .send(
          '<div class="error">作成に失敗しました</div>'
        );
    }
  };

  // ユーザーを更新
  update = async (req: Request, res: Response) => {
    try {
      const { id } = req.params;
      const data = req.body as UpdateUserRequest;

      // ユーザーが存在するか確認
      const user = await this.service.findById(id);
      if (!user) {
        return res
          .status(404)
          .send(
            '<div class="error">ユーザーが見つかりません</div>'
          );
      }

      // 更新実行
      const updated = await this.service.update(id, data);

      // 更新後のユーザーカードを返す
      const html = renderUserCard(updated);
      res.send(html);
    } catch (error) {
      res
        .status(500)
        .send(
          '<div class="error">更新に失敗しました</div>'
        );
    }
  };
}

上記のコードでは、バリデーションエラーやビジネスロジックエラーも HTML として返しています。これにより、クライアント側は一貫した方法でエラーを表示できます。

コントローラーの実装(編集フォームと削除)

typescript// userController.ts の続き
export class UserController {
  // ... 前のコードの続き

  // 編集フォームを返す
  edit = async (req: Request, res: Response) => {
    try {
      const { id } = req.params;
      const user = await this.service.findById(id);

      if (!user) {
        return res
          .status(404)
          .send(
            '<div class="error">ユーザーが見つかりません</div>'
          );
      }

      // 編集フォームの HTML を生成
      const html = renderUserEditForm(user);
      res.send(html);
    } catch (error) {
      res
        .status(500)
        .send(
          '<div class="error">エラーが発生しました</div>'
        );
    }
  };

  // ユーザーを削除
  delete = async (req: Request, res: Response) => {
    try {
      const { id } = req.params;

      // ユーザーが存在するか確認
      const user = await this.service.findById(id);
      if (!user) {
        return res
          .status(404)
          .send(
            '<div class="error">ユーザーが見つかりません</div>'
          );
      }

      // 削除実行
      await this.service.delete(id);

      // 空のレスポンスを返す(要素が DOM から削除される)
      res.status(204).send('');
    } catch (error) {
      res
        .status(500)
        .send(
          '<div class="error">削除に失敗しました</div>'
        );
    }
  };
}

上記のコードの edit メソッドは、編集フォームの HTML を返す専用のエンドポイントです。これにより、ユーザーカード表示と編集フォームを簡単に切り替えられます。

HTML テンプレート関数

typescript// templates.ts - HTML 生成関数
export function renderUserCard(user: User): string {
  const formattedDate =
    user.createdAt.toLocaleDateString('ja-JP');

  return `
    <div id="user-${user.id}" class="user-card">
      <div class="user-header">
        <h3>${escapeHtml(user.name)}</h3>
        <span class="user-id">#${user.id}</span>
      </div>

      <div class="user-info">
        <p><strong>メール:</strong> ${escapeHtml(
          user.email
        )}</p>
        <p><strong>登録日:</strong> ${formattedDate}</p>
      </div>

      <div class="user-actions">
        <button hx-get="/users/${user.id}/edit"
                hx-target="#user-${user.id}"
                hx-swap="outerHTML">
          編集
        </button>

        <button hx-delete="/users/${user.id}"
                hx-confirm="「${escapeHtml(
                  user.name
                )}」を削除しますか?"
                hx-target="#user-${user.id}"
                hx-swap="outerHTML">
          削除
        </button>
      </div>
    </div>
  `;
}

上記のコードでは、escapeHtml 関数を使って XSS 攻撃を防いでいます。ユーザー入力を HTML に埋め込む際は、必ずエスケープ処理を行いましょう。

typescript// templates.ts の続き
export function renderUserEditForm(user: User): string {
  return `
    <form id="user-${user.id}"
          class="user-edit-form"
          hx-put="/users/${user.id}"
          hx-target="#user-${user.id}"
          hx-swap="outerHTML">

      <div class="form-group">
        <label for="edit-name-${user.id}">名前:</label>
        <input type="text"
               id="edit-name-${user.id}"
               name="name"
               value="${escapeHtml(user.name)}"
               required>
      </div>

      <div class="form-group">
        <label for="edit-email-${
          user.id
        }">メールアドレス:</label>
        <input type="email"
               id="edit-email-${user.id}"
               name="email"
               value="${escapeHtml(user.email)}"
               required>
      </div>

      <div class="form-actions">
        <button type="submit">保存</button>

        <button type="button"
                hx-get="/users/${user.id}"
                hx-target="#user-${user.id}"
                hx-swap="outerHTML">
          キャンセル
        </button>
      </div>
    </form>
  `;
}

上記の編集フォームでは、キャンセルボタンが元の表示に戻すための GET リクエストを送信します。これにより、編集のキャンセルも統一的な方法で実現できます。

動作フローの全体像

以下の図は、CRUD 操作全体の動作フローを示しています。

mermaidstateDiagram-v2
  [*] --> UserList: 初回ロード
  UserList --> UserCard: ユーザー選択
  UserCard --> EditForm: 編集ボタン
  EditForm --> UserCard: 保存
  EditForm --> UserCard: キャンセル
  UserCard --> UserList: 削除
  UserList --> CreateForm: 新規作成
  CreateForm --> UserList: 作成完了
  UserList --> [*]

この状態遷移図から、各操作がシームレスに繋がっていることがわかります。すべての遷移は、HTML に含まれるリンクやフォームによって駆動されています。

図で理解できる要点

  • 各状態間の遷移は、HTML 内のリンクやフォームによって定義される
  • クライアント側のコードは状態遷移のロジックを持たない
  • サーバーが返す HTML が次の可能なアクションを決定する

まとめ

htmx を活用したハイパーメディア駆動設計により、Web アプリケーション開発のアプローチが大きく変わります。この記事で解説した内容をまとめましょう。

ハイパーメディア設計の利点

リンクとフォームを API として扱うことで、以下のメリットが得られます。

#メリット説明
1低い結合度フロントエンドが URL をハードコードする必要がない
2柔軟な変更API の URL 構造変更がクライアントに影響しない
3シンプルな実装JSON API とクライアント側の状態管理が不要
4自己記述的な APIHTML 自体が使い方のドキュメントになる
5段階的な導入既存のサーバーサイドレンダリングアプリに追加可能

開発者にとっての価値

htmx によるハイパーメディア設計は、開発者に以下の価値を提供します。

まず、複雑な JavaScript フレームワークを学習する必要がなくなります。HTML と HTTP の基本を理解していれば、すぐに使い始められるのです。

次に、フロントエンドとバックエンドの境界が曖昧になり、フルスタック開発がより容易になります。サーバー側で HTML を生成するため、データと表示ロジックを一箇所で管理できます。

さらに、ページ全体のリロードなしに動的な UI を実現できるため、ユーザーエクスペリエンスも向上します。

適用すべき場面

htmx のハイパーメディア設計は、以下のような場面で特に効果を発揮します。

管理画面や業務アプリケーションなど、複雑な UI よりも開発スピードが重視される場面では最適です。また、SEO が重要なコンテンツサイトでは、サーバーサイドレンダリングとの相性が抜群ですね。

既存のサーバーサイドアプリケーションに段階的にインタラクティブ性を追加したい場合も、htmx は理想的な選択肢となります。

今後の学習ステップ

この記事で紹介した基本的な概念を理解したら、以下のトピックに進んでみてください。

htmx の高度な機能(hx-boosthx-push-url、WebSocket サポートなど)を学ぶことで、より洗練されたアプリケーションを構築できます。

また、テンプレートエンジン(EJS、Pug、Handlebars など)を組み合わせることで、HTML 生成をより効率的に行えます。

さらに、HTMX と Alpine.js を組み合わせることで、クライアント側の軽量な状態管理も実現できますよ。

最後に

htmx によるハイパーメディア設計は、Web の本質に立ち返るアプローチです。リンクとフォームという Web の基本要素を API として活用することで、シンプルかつ強力なアプリケーションを構築できます。

複雑な JavaScript フレームワークに疲れた方、サーバーサイドレンダリングの良さを活かしたい方は、ぜひ htmx を試してみてください。きっと新しい発見があるはずです。

関連リンク