T-CREATOR

htmx 入門:ゼロから始める次世代フロントエンド開発

htmx 入門:ゼロから始める次世代フロントエンド開発

フロントエンド開発の世界は、ここ数年で驚くほど複雑になりました。React、Vue.js、Angular といった強力なフレームワークが主流となる一方で、多くの開発者が「学習コストの高さ」や「環境構築の複雑さ」に悩んでいるのではないでしょうか。

「簡単な機能を追加したいだけなのに、なぜこんなに複雑な設定が必要なの?」 「新しいフレームワークを覚える時間がない...」 「もっとシンプルに開発できる方法はないの?」

そんな悩みを抱える方に朗報です。htmx という革新的なライブラリが、フロントエンド開発の常識を変えようとしています。わずか 14KB の軽量ライブラリでありながら、HTML 属性だけでリッチなインタラクションを実現できる次世代のアプローチです。

今回は、htmx を完全にゼロから学び、実際に動作するアプリケーションを構築するまでの道のりを、初心者の方にもわかりやすくご紹介します。

フロントエンド開発の現在地

JavaScript フレームワークの複雑化

現代の Web 開発では、React、Vue.js、Angular といったフレームワークが圧倒的なシェアを占めています。これらのフレームワークは確かに強力ですが、同時に多くの課題も抱えています。

React プロジェクトの典型的な構成を見てみましょう:

bash# React プロジェクトの作成
yarn create react-app my-app
cd my-app

# よく必要になる追加パッケージ
yarn add react-router-dom
yarn add @reduxjs/toolkit react-redux
yarn add axios
yarn add @mui/material @emotion/react @emotion/styled
yarn add @testing-library/react @testing-library/jest-dom

この時点で、プロジェクトのサイズは既に数百 MB に達し、node_modules フォルダーには数千のパッケージが含まれています。

学習コストの高騰

現代のフロントエンド開発者が習得すべき技術は膨大です:

#技術領域必要な知識
1言語基礎JavaScript ES6+、TypeScript
2フレームワークReact Hooks、Vue Composition API
3状態管理Redux、Zustand、Pinia
4ルーティングReact Router、Vue Router
5ビルドツールWebpack、Vite、Rollup
6テストJest、Testing Library、Cypress
7スタイリングCSS-in-JS、Tailwind CSS、Sass

新人エンジニアがこれらを習得するには、12〜18 ヶ月もの期間が必要とされています。

開発効率の課題

実際の開発現場では、以下のような問題が頻発しています:

環境構築で発生する典型的なエラー

bash# Node.js バージョン不整合エラー
error @typescript-eslint/eslint-plugin@5.59.0:
The engine "node" is incompatible with this module.
Expected version "^12.22.0 || ^14.17.0 || >=16.0.0".

# 依存関係の競合エラー
npm ERR! peer dep missing: react@">=16.8.0", required by @testing-library/react-hooks@8.0.1

# ビルドエラー
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: Support for the experimental syntax 'jsx' isn't currently enabled

これらのエラーの解決に、本来の開発時間の 30〜40% を費やしてしまうケースも珍しくありません。

メンテナンスの負担も深刻です。依存関係の更新だけで以下のような作業が必要になります:

bash# セキュリティ脆弱性の警告
$ yarn audit
Found 47 vulnerabilities (12 moderate, 35 high)

# パッケージの更新作業
$ yarn upgrade-interactive --latest
# 数十のパッケージの更新判断が必要...

htmx という新しい選択肢

htmx とは何か

htmx(HyperText Markup eXtensions)は、2020 年に登場した革新的な JavaScript ライブラリです。**「HTML を中心とした Web 開発」**という、従来のフレームワークとは正反対のアプローチを提案しています。

htmx の基本思想は非常にシンプルです:

HTML は本来、ハイパーメディアとして強力な能力を持っている。 その能力を現代的に拡張すれば、複雑な JavaScript は不要になる。

従来の開発 vs htmx 開発の比較:

javascript// 従来のJavaScript(Ajax通信)
fetch('/api/users')
  .then((response) => response.json())
  .then((data) => {
    const userList = document.getElementById('users');
    userList.innerHTML = data
      .map((user) => `<li>${user.name}</li>`)
      .join('');
  })
  .catch((error) => {
    console.error('Error:', error);
  });
html<!-- htmx(HTML属性のみ) -->
<button hx-get="/api/users" hx-target="#users">
  ユーザー一覧を取得
</button>
<ul id="users"></ul>

この違いは一目瞭然です。htmx では HTML 属性だけで Ajax 通信が実現できています。

従来のアプローチとの違い

htmx と従来のフレームワークの根本的な違いを表で整理してみましょう:

#項目従来のフレームワークhtmx
1中心となる技術JavaScriptHTML
2データのやり取りJSONHTML フラグメント
3状態管理クライアントサイドサーバーサイド
4学習コスト高い(6〜12 ヶ月)低い(1〜2 週間)
5バンドルサイズ数百 KB〜数 MB14KB
6既存プロジェクトへの導入困難(リライトが必要)容易(段階的導入可能)

特に注目すべきはサーバーサイドでの状態管理です。従来の SPA では、クライアント側で複雑な状態管理を行う必要がありましたが、htmx ではサーバーが HTML を返すだけのシンプルな構造になります。

なぜ「次世代」なのか

htmx が「次世代」と呼ばれる理由は、単に新しいからではありません。Web 開発の本質に立ち返り、複雑さを取り除いたからです。

1. 学習曲線の劇的な改善

従来のフレームワークの学習曲線:

markdown複雑さ
  ^
  |     ●●●●●●●● (React習得)
  |   ●●
  | ●●
●●________________________> 時間
0    6ヶ月    12ヶ月

htmx の学習曲線:

markdown複雑さ
  ^
  |●●●●●●●●●●●●●●●●●●● (すぐに使える)
  |
  |
●●________________________> 時間
0      1週間     2週間

2. 開発生産性の向上

実際のプロジェクトでの開発時間比較:

#機能React 開発時間htmx 開発時間
1環境構築2〜4 時間5 分
2シンプルなフォーム1〜2 時間15 分
3Ajax 通信30 分〜1 時間5 分
4動的リスト更新1〜2 時間20 分

3. メンテナンス性の向上

htmx プロジェクトの package.json

json{
  "dependencies": {
    "express": "^4.18.2",
    "htmx.org": "^1.9.10"
  }
}

たった 2 つの依存関係です。セキュリティ脆弱性の心配も、複雑な更新作業も大幅に軽減されます。

4. パフォーマンスの優位性

  • 初期読み込み:14KB vs 数百 KB〜数 MB
  • ランタイム:DOM 操作のみ vs 仮想 DOM + 状態管理
  • メモリ使用量:最小限 vs フレームワーク + ライブラリ群

これらの特徴により、htmx は Web 開発を「原点回帰」させながら、同時に「次世代」の開発体験を提供しているのです。

はじめての htmx:基本の「き」

環境構築(最小構成)

htmx の最大の魅力は、環境構築の簡単さです。従来のフレームワークのような複雑な設定は一切必要ありません。

方法 1:CDN を使用した最速セットアップ

まずは、最も簡単な方法から始めましょう。HTML ファイル一つあれば十分です。

index.html

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>htmx 入門</title>
    <!-- htmx をCDNから読み込む(これだけ!) -->
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
  </head>
  <body>
    <h1>htmx 入門</h1>
    <p>準備完了です!</p>
  </body>
</html>

これだけで htmx を使う準備が整いました。所要時間:1 分

方法 2:Yarn を使用したプロジェクト構成

実際の開発では、サーバーサイドと組み合わせることが多いので、完全なプロジェクト構成も見てみましょう。

bash# プロジェクトの作成
mkdir htmx-tutorial
cd htmx-tutorial
yarn init -y

# 必要な依存関係をインストール
yarn add htmx.org express
yarn add -D nodemon

package.json の設定:

json{
  "name": "htmx-tutorial",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "htmx.org": "^1.9.10"
  },
  "devDependencies": {
    "nodemon": "^3.0.2"
  }
}

server.js

javascriptconst express = require('express');
const path = require('path');

const app = express();
const PORT = 3000;

// 静的ファイルの配信
app.use(express.static('public'));

// メインページ
app.get('/', (req, res) => {
  res.sendFile(
    path.join(__dirname, 'public', 'index.html')
  );
});

app.listen(PORT, () => {
  console.log(
    `サーバーが http://localhost:${PORT} で起動しました`
  );
});

public/index.html

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>htmx Tutorial</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
  </head>
  <body>
    <h1>htmx チュートリアル</h1>
    <p>サーバーが正常に動作しています!</p>
  </body>
</html>

サーバーを起動:

bashyarn dev

所要時間:5 分

最初の一歩:ボタンクリックで動的更新

環境が整ったので、早速 htmx の基本機能を体験してみましょう。

シンプルな時刻表示機能

server.js に API エンドポイントを追加:

javascript// ... existing code ...

// 現在時刻を返す API
app.get('/api/time', (req, res) => {
  const now = new Date().toLocaleString('ja-JP');
  res.send(`<p>現在時刻: <strong>${now}</strong></p>`);
});

// ... existing code ...

public/index.html を更新:

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>htmx Tutorial</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 40px;
      }
      button {
        padding: 10px 20px;
        background: #007bff;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }
      button:hover {
        background: #0056b3;
      }
      #time-display {
        margin-top: 20px;
        padding: 15px;
        border: 1px solid #ddd;
        border-radius: 4px;
      }
    </style>
  </head>
  <body>
    <h1>htmx チュートリアル</h1>

    <!-- これがhtmxの魔法! -->
    <button hx-get="/api/time" hx-target="#time-display">
      現在時刻を取得
    </button>

    <div id="time-display">ここに時刻が表示されます</div>
  </body>
</html>

ボタンをクリックしてみてください。JavaScript を一行も書かずに Ajax 通信が実行され、現在時刻が動的に表示されます!

HTML 属性の基本概念

先ほどの例で使用した hx-gethx-target について詳しく見てみましょう。

hx-get 属性

hx-get は HTTP GET リクエストを送信する属性です:

html<!-- 基本的な使用法 -->
<button hx-get="/api/data">データを取得</button>

<!-- 任意の要素で使用可能 -->
<div hx-get="/api/content">この div をクリックでも動作</div>
<span hx-get="/api/info">この span でも動作</span>

hx-target 属性

hx-target はレスポンスを挿入する場所を指定します:

html<!-- ID で指定 -->
<button hx-get="/api/data" hx-target="#result">取得</button>
<div id="result"></div>

<!-- CSS セレクターで指定 -->
<button hx-get="/api/data" hx-target=".content-area">
  取得
</button>
<div class="content-area"></div>

<!-- 相対的な指定 -->
<button hx-get="/api/data" hx-target="next div">
  取得
</button>
<div>ここに挿入される</div>

よくあるエラーとその解決法

初学者がよく遭遇するエラーをご紹介します:

エラー 1:htmx が読み込まれていない

csharpUncaught ReferenceError: htmx is not defined

解決法

html<!-- script タグが正しく配置されているか確認 -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>

エラー 2:サーバーが起動していない

bashGET http://localhost:3000/api/time net::ERR_CONNECTION_REFUSED

解決法

bash# サーバーが起動しているか確認
yarn dev

エラー 3:API エンドポイントが存在しない

bashGET http://localhost:3000/api/time 404 (Not Found)

解決法:server.js でエンドポイントが正しく定義されているか確認してください。

ステップアップ:基本機能を理解する

hx-get, hx-post の使い分け

htmx では、様々な HTTP メソッドを HTML 属性として使用できます。

hx-get:データの取得

hx-get はデータを取得する際に使用します:

html<!-- ユーザー情報の取得 -->
<button hx-get="/api/user/123" hx-target="#user-info">
  ユーザー情報を表示
</button>

<!-- 検索結果の取得 -->
<input
  type="text"
  hx-get="/api/search"
  hx-trigger="keyup changed delay:300ms"
  hx-target="#search-results"
  placeholder="検索キーワード"
/>

hx-post:データの送信

hx-post はデータを送信する際に使用します:

html<!-- フォームの送信 -->
<form hx-post="/api/contact" hx-target="#result">
  <input name="name" placeholder="お名前" required />
  <input
    name="email"
    type="email"
    placeholder="メールアドレス"
    required
  />
  <textarea
    name="message"
    placeholder="メッセージ"
  ></textarea>
  <button type="submit">送信</button>
</form>
<div id="result"></div>

server.js でのフォーム処理:

javascript// フォームデータの解析を有効化
app.use(express.urlencoded({ extended: true }));

// フォーム送信の処理
app.post('/api/contact', (req, res) => {
  const { name, email, message } = req.body;

  // 簡単なバリデーション
  if (!name || !email || !message) {
    res.status(400).send(`
      <div style="color: red; padding: 10px; border: 1px solid red;">
        エラー: すべての項目を入力してください
      </div>
    `);
    return;
  }

  // 成功レスポンス
  res.send(`
    <div style="color: green; padding: 10px; border: 1px solid green;">
      ${name}さん、お問い合わせありがとうございます!<br>
      ${email} に確認メールを送信しました。
    </div>
  `);
});

hx-target でのターゲット指定

hx-target は非常に柔軟なターゲット指定が可能です。

基本的な指定方法

html<!-- ID での指定 -->
<button hx-get="/api/data" hx-target="#result">取得</button>

<!-- クラスでの指定 -->
<button hx-get="/api/data" hx-target=".result-area">
  取得
</button>

<!-- 要素タイプでの指定 -->
<button hx-get="/api/data" hx-target="p">取得</button>

相対的な指定

html<!-- 最も近い親要素 -->
<div>
  <button hx-get="/api/data" hx-target="closest div">
    更新
  </button>
  <p>この div が更新されます</p>
</div>

<!-- 次の兄弟要素 -->
<button hx-get="/api/data" hx-target="next .result">
  取得
</button>
<div class="result">ここが更新されます</div>

<!-- 前の兄弟要素 -->
<div class="result">ここが更新されます</div>
<button hx-get="/api/data" hx-target="previous .result">
  取得
</button>

hx-trigger でのイベント制御

hx-trigger を使うことで、リクエストを発火するタイミングを細かく制御できます。

基本的なイベント

html<!-- クリック時(デフォルト) -->
<button hx-get="/api/data">クリック</button>

<!-- マウスオーバー時 -->
<div hx-get="/api/preview" hx-trigger="mouseenter">
  ホバーして詳細を表示
</div>

<!-- フォーカス時 -->
<input hx-get="/api/suggestions" hx-trigger="focus" />

<!-- 値変更時 -->
<select hx-get="/api/filter" hx-trigger="change">
  <option value="all">すべて</option>
  <option value="active">アクティブ</option>
</select>

高度なトリガー設定

html<!-- 遅延実行 -->
<input
  hx-get="/api/search"
  hx-trigger="keyup changed delay:500ms"
  placeholder="入力後0.5秒で検索"
/>

<!-- 複数イベント -->
<div hx-get="/api/data" hx-trigger="click, keyup from:body">
  クリックまたはキー入力で実行
</div>

<!-- 条件付き実行 -->
<button
  hx-get="/api/premium-feature"
  hx-trigger="click[this.classList.contains('premium')]"
>
  プレミアム機能
</button>

<!-- 定期実行 -->
<div
  hx-get="/api/status"
  hx-trigger="every 30s"
  hx-target="this"
>
  30秒ごとに更新されるステータス
</div>

実践的な検索機能の実装

これまでの知識を組み合わせて、リアルタイム検索機能を作ってみましょう:

server.js に検索 API を追加:

javascript// サンプルデータ
const users = [
  {
    id: 1,
    name: '田中太郎',
    email: 'tanaka@example.com',
    role: 'エンジニア',
  },
  {
    id: 2,
    name: '佐藤花子',
    email: 'sato@example.com',
    role: 'デザイナー',
  },
  {
    id: 3,
    name: '鈴木一郎',
    email: 'suzuki@example.com',
    role: 'マネージャー',
  },
  {
    id: 4,
    name: '高橋美咲',
    email: 'takahashi@example.com',
    role: 'エンジニア',
  },
  {
    id: 5,
    name: '山田健太',
    email: 'yamada@example.com',
    role: 'セールス',
  },
];

// 検索 API
app.get('/api/search', (req, res) => {
  const query = req.query.q || '';

  if (query.trim() === '') {
    res.send('<p>検索キーワードを入力してください</p>');
    return;
  }

  const results = users.filter(
    (user) =>
      user.name.includes(query) ||
      user.email.includes(query) ||
      user.role.includes(query)
  );

  if (results.length === 0) {
    res.send(
      `<p>「${query}」に一致するユーザーが見つかりませんでした</p>`
    );
    return;
  }

  const html = results
    .map(
      (user) => `
    <div style="border: 1px solid #ddd; padding: 10px; margin: 5px 0;">
      <strong>${user.name}</strong> (${user.role})<br>
      <small>${user.email}</small>
    </div>
  `
    )
    .join('');

  res.send(html);
});

HTML 部分

html<div>
  <h2>ユーザー検索</h2>
  <input
    type="text"
    name="q"
    hx-get="/api/search"
    hx-trigger="keyup changed delay:300ms"
    hx-target="#search-results"
    hx-include="this"
    placeholder="名前、メール、役職で検索..."
    style="width: 100%; padding: 10px; font-size: 16px;"
  />

  <div id="search-results" style="margin-top: 20px;">
    検索キーワードを入力してください
  </div>
</div>

この実装により、300ms の遅延付きリアルタイム検索が実現できます。ユーザーが入力を止めてから 300ms 後に検索が実行されるため、無駄なリクエストを避けることができます。

実践編:小さなアプリを作ってみる

カウンターアプリの作成

htmx の基本を理解したところで、実際にインタラクティブなアプリケーションを作ってみましょう。まずはシンプルなカウンターアプリから始めます。

サーバーサイドの実装

server.js にカウンター機能を追加:

javascript// カウンターの状態(実際の開発ではデータベースを使用)
let counter = 0;

// カウンター表示
app.get('/api/counter', (req, res) => {
  res.send(`
    <div style="text-align: center; font-size: 24px; padding: 20px;">
      現在のカウント: <strong>${counter}</strong>
    </div>
  `);
});

// カウンターを増加
app.post('/api/counter/increment', (req, res) => {
  counter++;
  res.send(`
    <div style="text-align: center; font-size: 24px; padding: 20px;">
      現在のカウント: <strong>${counter}</strong>
    </div>
  `);
});

// カウンターを減少
app.post('/api/counter/decrement', (req, res) => {
  counter--;
  res.send(`
    <div style="text-align: center; font-size: 24px; padding: 20px;">
      現在のカウント: <strong>${counter}</strong>
    </div>
  `);
});

// カウンターをリセット
app.post('/api/counter/reset', (req, res) => {
  counter = 0;
  res.send(`
    <div style="text-align: center; font-size: 24px; padding: 20px;">
      現在のカウント: <strong>${counter}</strong>
    </div>
  `);
});

フロントエンドの実装

public/counter.html を作成:

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>htmx カウンターアプリ</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 600px;
        margin: 0 auto;
        padding: 40px;
        text-align: center;
      }

      .counter-display {
        background: #f8f9fa;
        border: 2px solid #dee2e6;
        border-radius: 8px;
        margin: 20px 0;
      }

      .button-group {
        display: flex;
        justify-content: center;
        gap: 10px;
        margin: 20px 0;
      }

      button {
        padding: 12px 24px;
        font-size: 16px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        transition: background-color 0.2s;
      }

      .btn-primary {
        background: #007bff;
        color: white;
      }
      .btn-primary:hover {
        background: #0056b3;
      }

      .btn-secondary {
        background: #6c757d;
        color: white;
      }
      .btn-secondary:hover {
        background: #545b62;
      }

      .btn-danger {
        background: #dc3545;
        color: white;
      }
      .btn-danger:hover {
        background: #c82333;
      }
    </style>
  </head>
  <body>
    <h1>htmx カウンターアプリ</h1>

    <!-- カウンター表示エリア -->
    <div
      id="counter-display"
      class="counter-display"
      hx-get="/api/counter"
      hx-trigger="load"
    >
      読み込み中...
    </div>

    <!-- ボタングループ -->
    <div class="button-group">
      <button
        class="btn-primary"
        hx-post="/api/counter/increment"
        hx-target="#counter-display"
      >
        ➕ 増加
      </button>

      <button
        class="btn-secondary"
        hx-post="/api/counter/decrement"
        hx-target="#counter-display"
      >
        ➖ 減少
      </button>

      <button
        class="btn-danger"
        hx-post="/api/counter/reset"
        hx-target="#counter-display"
      >
        🔄 リセット
      </button>
    </div>
  </body>
</html>

この実装のポイント:

  • hx-trigger="load":ページ読み込み時に初期値を取得
  • 状態管理:サーバーサイドで状態を管理(シンプル!)
  • 再利用可能:各ボタンが同じターゲットを更新

フォーム送信とバリデーション

次に、より実用的なフォーム処理を実装してみましょう。ユーザー登録フォームを作成します。

バリデーション付きフォーム処理

server.js にユーザー登録機能を追加:

javascript// ユーザーデータの保存(実際の開発ではデータベースを使用)
const users = [];

// ユーザー登録処理
app.post('/api/users', (req, res) => {
  const { name, email, age } = req.body;

  // バリデーションエラーのチェック
  const errors = [];

  if (!name || name.trim().length < 2) {
    errors.push('名前は2文字以上で入力してください');
  }

  if (!email || !email.includes('@')) {
    errors.push('有効なメールアドレスを入力してください');
  }

  if (!age || isNaN(age) || age < 0 || age > 150) {
    errors.push('年齢は0〜150の数値で入力してください');
  }

  // 重複チェック
  if (users.find((user) => user.email === email)) {
    errors.push('このメールアドレスは既に登録されています');
  }

  // エラーがある場合
  if (errors.length > 0) {
    const errorHtml = errors
      .map(
        (error) => `<li style="color: red;">${error}</li>`
      )
      .join('');

    res.status(400).send(`
      <div style="border: 1px solid red; background: #ffe6e6; padding: 15px; border-radius: 4px;">
        <h4 style="color: red; margin: 0 0 10px 0;">入力エラー</h4>
        <ul style="margin: 0; padding-left: 20px;">
          ${errorHtml}
        </ul>
      </div>
    `);
    return;
  }

  // ユーザーを追加
  const newUser = {
    id: users.length + 1,
    name: name.trim(),
    email: email.trim(),
    age: parseInt(age),
    createdAt: new Date().toLocaleString('ja-JP'),
  };

  users.push(newUser);

  // 成功レスポンス
  res.send(`
    <div style="border: 1px solid green; background: #e6ffe6; padding: 15px; border-radius: 4px;">
      <h4 style="color: green; margin: 0 0 10px 0;">登録完了</h4>
      <p><strong>${newUser.name}</strong>さんの登録が完了しました!</p>
      <p>登録日時: ${newUser.createdAt}</p>
      <button onclick="location.reload()" style="margin-top: 10px; padding: 8px 16px;">
        新規登録を続ける
      </button>
    </div>
  `);
});

// ユーザー一覧取得
app.get('/api/users', (req, res) => {
  if (users.length === 0) {
    res.send('<p>まだユーザーが登録されていません</p>');
    return;
  }

  const userHtml = users
    .map(
      (user) => `
    <tr>
      <td>${user.id}</td>
      <td>${user.name}</td>
      <td>${user.email}</td>
      <td>${user.age}</td>
      <td>${user.createdAt}</td>
    </tr>
  `
    )
    .join('');

  res.send(`
    <table style="width: 100%; border-collapse: collapse;">
      <thead>
        <tr style="background: #f8f9fa;">
          <th style="border: 1px solid #ddd; padding: 8px;">ID</th>
          <th style="border: 1px solid #ddd; padding: 8px;">名前</th>
          <th style="border: 1px solid #ddd; padding: 8px;">メール</th>
          <th style="border: 1px solid #ddd; padding: 8px;">年齢</th>
          <th style="border: 1px solid #ddd; padding: 8px;">登録日時</th>
        </tr>
      </thead>
      <tbody>
        ${userHtml}
      </tbody>
    </table>
  `);
});

フォームの HTML

public/form.html を作成:

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>htmx フォームデモ</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
      }

      .form-group {
        margin-bottom: 20px;
      }

      label {
        display: block;
        margin-bottom: 5px;
        font-weight: bold;
      }

      input,
      textarea {
        width: 100%;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 16px;
      }

      button {
        background: #28a745;
        color: white;
        padding: 12px 24px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 16px;
      }

      button:hover {
        background: #218838;
      }

      button:disabled {
        background: #6c757d;
        cursor: not-allowed;
      }
    </style>
  </head>
  <body>
    <h1>ユーザー登録フォーム</h1>

    <!-- 登録フォーム -->
    <form hx-post="/api/users" hx-target="#form-result">
      <div class="form-group">
        <label for="name">名前 *</label>
        <input
          type="text"
          id="name"
          name="name"
          placeholder="山田太郎"
          required
        />
      </div>

      <div class="form-group">
        <label for="email">メールアドレス *</label>
        <input
          type="email"
          id="email"
          name="email"
          placeholder="yamada@example.com"
          required
        />
      </div>

      <div class="form-group">
        <label for="age">年齢 *</label>
        <input
          type="number"
          id="age"
          name="age"
          min="0"
          max="150"
          placeholder="25"
          required
        />
      </div>

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

    <!-- 結果表示エリア -->
    <div id="form-result" style="margin-top: 20px;"></div>

    <hr style="margin: 40px 0;" />

    <!-- ユーザー一覧 -->
    <h2>登録済みユーザー</h2>
    <button
      hx-get="/api/users"
      hx-target="#user-list"
      style="background: #007bff;"
    >
      一覧を表示
    </button>

    <div id="user-list" style="margin-top: 20px;"></div>
  </body>
</html>

動的リスト操作

最後に、リアルタイムでアイテムの追加・削除・編集ができる TODO アプリを作成しましょう。

TODO アプリのサーバーサイド

server.js に TODO 機能を追加:

javascript// TODO データ
let todos = [
  { id: 1, text: 'htmx を学ぶ', completed: false },
  { id: 2, text: 'デモアプリを作る', completed: true },
];
let nextTodoId = 3;

// TODO 一覧取得
app.get('/api/todos', (req, res) => {
  const todoHtml = todos
    .map(
      (todo) => `
    <li id="todo-${todo.id}" style="
      display: flex; 
      align-items: center; 
      padding: 10px; 
      border: 1px solid #ddd; 
      margin: 5px 0; 
      border-radius: 4px;
      ${
        todo.completed
          ? 'background: #f8f9fa; text-decoration: line-through;'
          : ''
      }
    ">
      <span style="flex: 1;">${todo.text}</span>
      <button 
        hx-patch="/api/todos/${todo.id}/toggle" 
        hx-target="#todo-${todo.id}"
        style="margin: 0 5px; padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; ${
          todo.completed
            ? 'background: #ffc107; color: black;'
            : 'background: #28a745; color: white;'
        }">
        ${todo.completed ? '戻す' : '完了'}
      </button>
      <button 
        hx-delete="/api/todos/${todo.id}" 
        hx-target="#todo-${todo.id}"
        hx-swap="outerHTML"  
        hx-confirm="本当に削除しますか?"
        style="margin: 0 5px; padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
        削除
      </button>
    </li>
  `
    )
    .join('');

  res.send(
    todos.length > 0 ? todoHtml : '<p>TODO がありません</p>'
  );
});

// TODO 追加
app.post('/api/todos', (req, res) => {
  const { text } = req.body;

  if (!text || text.trim() === '') {
    res.status(400).send(`
      <div style="color: red; padding: 10px; border: 1px solid red; border-radius: 4px;">
        TODO の内容を入力してください
      </div>
    `);
    return;
  }

  const newTodo = {
    id: nextTodoId++,
    text: text.trim(),
    completed: false,
  };

  todos.push(newTodo);

  // 新しい TODO アイテムを返す
  res.send(`
    <li id="todo-${newTodo.id}" style="
      display: flex; 
      align-items: center; 
      padding: 10px; 
      border: 1px solid #ddd; 
      margin: 5px 0; 
      border-radius: 4px;
    ">
      <span style="flex: 1;">${newTodo.text}</span>
      <button 
        hx-patch="/api/todos/${newTodo.id}/toggle" 
        hx-target="#todo-${newTodo.id}"
        style="margin: 0 5px; padding: 5px 10px; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
        完了
      </button>
      <button 
        hx-delete="/api/todos/${newTodo.id}" 
        hx-target="#todo-${newTodo.id}"
        hx-swap="outerHTML"
        hx-confirm="本当に削除しますか?"
        style="margin: 0 5px; padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
        削除
      </button>
    </li>
  `);
});

// TODO 完了状態切り替え
app.patch('/api/todos/:id/toggle', (req, res) => {
  const id = parseInt(req.params.id);
  const todo = todos.find((t) => t.id === id);

  if (!todo) {
    res
      .status(404)
      .send(
        '<p style="color: red;">TODO が見つかりません</p>'
      );
    return;
  }

  todo.completed = !todo.completed;

  // 更新された TODO アイテムを返す
  res.send(`
    <li id="todo-${todo.id}" style="
      display: flex; 
      align-items: center; 
      padding: 10px; 
      border: 1px solid #ddd; 
      margin: 5px 0; 
      border-radius: 4px;
      ${
        todo.completed
          ? 'background: #f8f9fa; text-decoration: line-through;'
          : ''
      }
    ">
      <span style="flex: 1;">${todo.text}</span>
      <button 
        hx-patch="/api/todos/${todo.id}/toggle" 
        hx-target="#todo-${todo.id}"
        style="margin: 0 5px; padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; ${
          todo.completed
            ? 'background: #ffc107; color: black;'
            : 'background: #28a745; color: white;'
        }">
        ${todo.completed ? '戻す' : '完了'}
      </button>
      <button 
        hx-delete="/api/todos/${todo.id}" 
        hx-target="#todo-${todo.id}"
        hx-swap="outerHTML"
        hx-confirm="本当に削除しますか?"
        style="margin: 0 5px; padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
        削除
      </button>
    </li>
  `);
});

// TODO 削除
app.delete('/api/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  todos = todos.filter((t) => t.id !== id);
  res.send(''); // 空のレスポンスで要素を削除
});

TODO アプリの HTML

public/todo.html を作成:

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>htmx TODO アプリ</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 600px;
        margin: 0 auto;
        padding: 20px;
      }

      .add-form {
        display: flex;
        margin-bottom: 20px;
      }

      .add-form input {
        flex: 1;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 4px 0 0 4px;
        font-size: 16px;
      }

      .add-form button {
        padding: 10px 20px;
        background: #007bff;
        color: white;
        border: none;
        border-radius: 0 4px 4px 0;
        cursor: pointer;
      }

      #todo-list {
        list-style: none;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <h1>htmx TODO アプリ</h1>

    <!-- TODO 追加フォーム -->
    <form
      hx-post="/api/todos"
      hx-target="#todo-list"
      hx-swap="beforeend"
      hx-on::after-request="this.reset()"
      class="add-form"
    >
      <input
        type="text"
        name="text"
        placeholder="新しい TODO を入力..."
        required
      />
      <button type="submit">追加</button>
    </form>

    <!-- TODO 一覧 -->
    <ul
      id="todo-list"
      hx-get="/api/todos"
      hx-trigger="load"
    >
      読み込み中...
    </ul>
  </body>
</html>

より実用的な機能を学ぶ

エラーハンドリング

実際のアプリケーションでは、適切なエラーハンドリングが重要です。htmx でのエラー処理方法を学びましょう。

JavaScript によるエラーハンドリング

html<script>
  // レスポンスエラーの処理
  document.addEventListener(
    'htmx:responseError',
    function (event) {
      const statusCode = event.detail.xhr.status;
      const errorMessage = event.detail.xhr.responseText;

      let alertMessage = 'エラーが発生しました';

      switch (statusCode) {
        case 400:
          alertMessage = `入力エラー: ${errorMessage}`;
          break;
        case 401:
          alertMessage =
            '認証が必要です。ログインしてください。';
          break;
        case 403:
          alertMessage = 'アクセス権限がありません。';
          break;
        case 404:
          alertMessage =
            '要求されたリソースが見つかりません。';
          break;
        case 500:
          alertMessage =
            'サーバー内部エラーが発生しました。管理者に連絡してください。';
          break;
        default:
          alertMessage = `HTTP ${statusCode}: ${errorMessage}`;
      }

      // エラーメッセージを表示
      const errorDiv = document.createElement('div');
      errorDiv.innerHTML = `
    <div style="
      position: fixed; 
      top: 20px; 
      right: 20px; 
      background: #f8d7da; 
      color: #721c24; 
      padding: 15px; 
      border: 1px solid #f5c6cb; 
      border-radius: 4px;
      max-width: 300px;
      z-index: 1000;
    ">
      <strong>エラー</strong><br>
      ${alertMessage}
      <button onclick="this.parentElement.parentElement.remove()" 
              style="float: right; background: none; border: none; font-size: 18px;">
        ×
      </button>
    </div>
  `;
      document.body.appendChild(errorDiv);

      // 5秒後に自動で削除
      setTimeout(() => {
        if (errorDiv.parentElement) {
          errorDiv.remove();
        }
      }, 5000);
    }
  );

  // ネットワークエラーの処理
  document.addEventListener(
    'htmx:sendError',
    function (event) {
      alert(
        'ネットワークエラー: サーバーに接続できません。インターネット接続を確認してください。'
      );
    }
  );
</script>

ローディング状態の表示

ユーザビリティ向上のため、リクエスト中のローディング表示を実装しましょう。

CSS でのローディング表示

css/* ローディング時のスタイル */
.htmx-request {
  opacity: 0.6;
  pointer-events: none;
}

.htmx-request::after {
  content: ' 読み込み中...';
  color: #666;
  font-style: italic;
}

/* スピナーアニメーション */
.spinner {
  display: none;
  width: 20px;
  height: 20px;
  border: 2px solid #f3f3f3;
  border-top: 2px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-left: 10px;
}

.htmx-request .spinner {
  display: inline-block;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
html<!-- ローディングスピナー付きボタン -->
<button hx-get="/api/slow-operation" hx-target="#result">
  データを取得
  <span class="spinner"></span>
</button>

アニメーション効果

htmx は CSS トランジションと組み合わせることで、滑らかなアニメーション効果を実現できます。

フェードイン/アウト効果

css/* アニメーション用 CSS */
.fade-in {
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

.fade-in.htmx-added {
  opacity: 1;
}

.fade-out {
  opacity: 1;
  transition: opacity 0.3s ease-in-out;
}

.fade-out.htmx-swapping {
  opacity: 0;
}

.slide-down {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease-out;
}

.slide-down.htmx-added {
  max-height: 1000px;
}
html<!-- アニメーション付きの要素 -->
<div
  id="animated-content"
  class="fade-in fade-out"
  hx-get="/api/content"
  hx-trigger="load"
>
  初期コンテンツ
</div>

<button
  hx-get="/api/new-content"
  hx-target="#animated-content"
  hx-swap="innerHTML transition:true"
>
  アニメーション付きで更新
</button>

実用的なトースト通知

javascript// トースト通知を表示する関数
function showToast(message, type = 'info') {
  const toast = document.createElement('div');
  toast.className = `toast toast-${type}`;
  toast.innerHTML = `
    <div style="
      position: fixed;
      top: 20px;
      right: 20px;
      padding: 15px 20px;
      border-radius: 4px;
      color: white;
      font-weight: bold;
      z-index: 1000;
      transform: translateX(100%);
      transition: transform 0.3s ease-in-out;
      ${type === 'success' ? 'background: #28a745;' : ''}
      ${type === 'error' ? 'background: #dc3545;' : ''}
      ${type === 'info' ? 'background: #007bff;' : ''}
    ">
      ${message}
    </div>
  `;

  document.body.appendChild(toast);

  // アニメーション開始
  setTimeout(() => {
    toast.firstElementChild.style.transform =
      'translateX(0)';
  }, 100);

  // 3秒後に削除
  setTimeout(() => {
    toast.firstElementChild.style.transform =
      'translateX(100%)';
    setTimeout(() => toast.remove(), 300);
  }, 3000);
}

// htmx イベントと連携
document.addEventListener(
  'htmx:afterRequest',
  function (event) {
    if (event.detail.successful) {
      showToast('操作が完了しました', 'success');
    }
  }
);

まとめ

htmx を使った次世代フロントエンド開発について、基礎から実践まで幅広くご紹介しました。

htmx の主な利点

学習コストの圧倒的な低さ:HTML の知識があれば、すぐに使い始めることができます。React や Vue.js のような複雑な概念を学ぶ必要がありません。

開発効率の向上:環境構築からアプリケーション完成まで、従来のフレームワークの 1/3 から 1/5 の時間で開発できます。

メンテナンス性:依存関係が少なく、セキュリティリスクや更新作業が大幅に軽減されます。

既存プロジェクトとの親和性:段階的な導入が可能で、既存の Web アプリケーションに簡単に組み込むことができます。

実践での活用場面

htmx は以下のような場面で特に威力を発揮します:

  • 管理画面や内部ツールの開発
  • プロトタイプや MVP の迅速な構築
  • 既存の Web アプリケーションへの段階的な機能追加
  • 学習コストを抑えたい小〜中規模チームでの開発

次のステップ

htmx をさらに活用するために、以下の学習を進めてみてください:

  1. サーバーサイドフレームワークとの統合(Rails、Django、Laravel など)
  2. WebSocket を使ったリアルタイム通信
  3. Progressive Web App (PWA) 対応
  4. テスト手法の習得

htmx は、Web 開発の複雑さに疲れた多くの開発者にとって、新鮮で効率的な選択肢となるでしょう。ぜひ実際のプロジェクトで試してみて、その魅力を体感してください!

関連リンク