DjangoとHTMXで動的Web開発をシンプルに:FormとSerializerの活用



前回の記事では、HTMXがデータをサーバーに送信する方法について詳しく解説しました。

前回の記事DjangoとHTMXで動的Web開発をシンプルに(第4回):ペイロードの送信方法

従来のJavaScriptのfetch()がJSONペイロードを直接作成して送信する方法に近いとすれば、HTMXはDOMから値を収集し、フォームデータのように送信する方法に近いことを確認しましたね。

では、ここで自然と疑問が湧いてきます。

「HTMXで送られてきたこのデータを、Djangoでは何を使って検証するのが最も自然なのだろうか?」

最初に多くの方がDRF Serializerを思い浮かべるかもしれません。実際にSerializerは強力なバリデーションツールであり、JSON形式でなくても使用できます。しかし、いざHTMXと組み合わせてみると、どうも不自然に感じることもあります。

なぜでしょうか?

理由は単純です。 HTMXの基本的な流れはHTMLフォームの世界に近く、Djangoにはその世界のために設計されたFormがすでに存在するからです。

この記事では、HTMXリクエストの処理においてDjango FormDRF Serializerをそれぞれどのように活用できるか、そしてどちらがより自然で実用的な選択肢なのかを整理していきます。

HTMXリクエスト処理におけるFormとSerializerの役割比較


HTMXが送信するデータは結局「フォームデータ」に近い

前回の記事で見たように、HTMXは基本的にHTML要素の値を収集してサーバーに送信します。<form>内の入力値を送ったり、hx-includeで特定の要素を含めたり、hx-valsで追加の値を付加したりする、といった具合です。

つまり、HTMXの基本的な哲学は以下の点に集約されます。

  • JavaScriptオブジェクトを直接組み立てない
  • HTML要素から値を集める
  • サーバーにリクエストを送信する
  • JSONよりもHTMLの断片応答と相性が良い

この流れは、想像以上に非常に重要です。なぜなら、この構造は典型的なDjango Formの処理フローとほぼ一致するからです。

Django Formもまた、以下のような前提で設計されています。

  • ユーザーがHTMLフォームに入力する
  • サーバーがrequest.POSTを受け取る
  • Formがデータを検証する
  • エラーがあれば再レンダリングする
  • 問題がなければ保存し、結果を応答する

こうなると、HTMXとDjango Formはまるでシンデレラとガラスの靴のようにぴったりと合うような気がしてきます。

したがって、このような結論を導き出しても良いのではないでしょうか。

HTMXと最も自然に連携するDjangoの検証ツールは、DRF SerializerよりもDjango Formである。


なぜDjango Formの方が相性が良いのか



DRF Serializerはもちろん優れたツールです。 しかし、本来の設計の文脈を考えると、SerializerはデータシリアライズとAPI入力検証により適したツールです。

一方、Django Formは最初から以下の目的のために作られました。

  • HTMLフォーム入力の処理
  • サーバーサイドのバリデーション
  • エラーメッセージの表示
  • 入力値の保持
  • テンプレートの再レンダリング

つまり、HTMXのように「HTMLの一部を再取得して置き換える方式」と組み合わせたときに、はるかに自然な形で機能します。

例えば、考えてみましょう。

ユーザーがコメント作成フォームを送信します。

  • バリデーションに失敗したら?
  • 入力内容は保持されるべきです
  • どのフィールドに問題があるか表示する必要があります
  • エラーが付加されたフォームを部分的に再レンダリングする必要があります

このようなUXは、Django Formが非常に得意とするところです。

もちろん、Serializerもerrorsを提供し、検証も可能です。しかし、次のステップである「HTMLフォームのUX全体を再構成する作業」は、Formの方がはるかにスムーズです。

そのため、HTMXとの相性だけを考えると、Formが基本的な選択肢であると考えるのが適切です。


最も自然な組み合わせ:HTMX + Django Form

まず、最も基本的な例を見てみましょう。簡単なTODO登録フォームです。

Formの定義

from django import forms

class TodoForm(forms.Form):
    title = forms.CharField(max_length=100, label="제목")
    priority = forms.IntegerField(min_value=1, max_value=5, label="우선순위")

これで、ビューではrequest.POSTをFormにそのまま渡して検証すれば良いだけです。

Viewの処理

from django.shortcuts import render
from django.http import HttpResponse

def todo_create(request):
    if request.method == "POST":
        form = TodoForm(request.POST)
        if form.is_valid():
            title = form.cleaned_data["title"]
            priority = form.cleaned_data["priority"]

            # 保存ロジックを実行
            # Todo.objects.create(title=title, priority=priority)

            return render(request, "todos/partials/todo_item.html", {
                "title": title,
                "priority": priority,
            })

        return render(request, "todos/partials/todo_form.html", {
            "form": form,
        }, status=400)

    form = TodoForm()
    return render(request, "todos/partials/todo_form.html", {
        "form": form,
    })

ここでのポイントは非常に明確です。

  1. HTMXが送信した値をrequest.POSTで受け取る
  2. Formが検証する
  3. 成功すればHTMLのパーシャルを返す
  4. 失敗すればエラーを含むFormを再レンダリングする

この流れはDjangoらしく、HTMXらしく、そして何よりもメンテナンスが容易です。


テンプレートでエラーを自然に再表示する

Django Formが特に輝く部分はまさにここです。

検証失敗時、Formオブジェクトにはすでに以下の情報が含まれています。

  • ユーザーが入力した値
  • フィールドごとのエラーメッセージ
  • 非フィールドエラー
  • どのフィールドが無効であるかの状態

したがって、パーシャルテンプレートでは、これらをそのままレンダリングすれば良いのです。

todo_form.html

<form hx-post="{% url 'todo_create' %}" hx-target="#todo-form-wrap" hx-swap="outerHTML">
    {% csrf_token %}

    {% if form.non_field_errors %}
        <div class="error-box">
            {{ form.non_field_errors }}
        </div>
    {% endif %}

    <div>
        <label for="{{ form.title.id_for_label }}">제목</label>
        {{ form.title }}
        {% if form.title.errors %}
            <div class="field-error">{{ form.title.errors }}</div>
        {% endif %}
    </div>

    <div>
        <label for="{{ form.priority.id_for_label }}">우선순위</label>
        {{ form.priority }}
        {% if form.priority.errors %}
            <div class="field-error">{{ form.priority.errors }}</div>
        {% endif %}
    </div>

    <button type="submit">등록</button>
</form>

この例は非常に単純ですが、HTMXとDjango Formの相性の良さをよく示しています。

  • 失敗時にはフォーム全体を再レンダリングでき
  • ユーザーが入力した値も保持され
  • エラーメッセージもフィールドの横に自然に表示されます

従来のfetch() + JSON + 手動DOM操作方式であれば、このようなフローを実装するためにJavaScriptをかなり多く書く必要があったでしょう。しかし、HTMXとDjango Formの組み合わせでは、サーバーサイドのコードとテンプレートだけで、はるかにシンプルに処理できます。


ではSerializerは不要なのか?

Serializerは不要だという言い方は言い過ぎで、このくらいで表現するのが良いのではないでしょうか。 あえてSerializerをデフォルトで選択する必要はない、といったところでしょうか?

実際、DRF Serializerは、以下のような状況では依然として十分に魅力的です。

  • すでにプロジェクト全体でDRFを積極的に使用している場合
  • 同じ検証ロジックをAPIとサーバーレンダリング画面で共有したい場合
  • 入力検証ロジックが複雑で、それをSerializerにすでにうまくまとめてある場合
  • 後で同じ機能をモバイルアプリや外部APIにも公開する計画がある場合

つまり、Serializerは「HTMXで使えるか?」の問題ではなく、 「あえてここでSerializerを使うことが、全体的なアーキテクチャ上でメリットがあるか?」の問題に近いでしょう。


Serializerも使用は可能

例えば、すでに以下のようなSerializerがあると仮定しましょう。

from rest_framework import serializers

class TodoSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=100)
    priority = serializers.IntegerField(min_value=1, max_value=5)

この場合、HTMXリクエストでも十分に活用できます。

from django.shortcuts import render

def todo_create_with_serializer(request):
    if request.method == "POST":
        serializer = TodoSerializer(data=request.POST)

        if serializer.is_valid():
            title = serializer.validated_data["title"]
            priority = serializer.validated_data["priority"]

            return render(request, "todos/partials/todo_item.html", {
                "title": title,
                "priority": priority,
            })

        return render(request, "todos/partials/todo_form_serializer.html", {
            "errors": serializer.errors,
            "data": request.POST,
        }, status=400)

ご覧の通り、技術的には全く問題ありません。 SerializerはJSONのみを受け取るツールではないからです。

しかし、ここで微妙な違いが明らかになります。

Formを使用する場合:

  • 入力値の保持
  • フィールドごとのレンダリング
  • エラーバインディング
  • テンプレートとの連携

これらが自然に連携します。

一方、Serializerを使用する場合:

  • serializer.errorsをテンプレートの構造に合わせて直接展開する必要があり
  • 既存の入力値も別途渡す必要があり
  • HTMLフォームの再レンダリングとの連携を開発者がより多く考慮する必要があります

つまり、使用は可能ですが、手作業が少し増える傾向があります。

まさにこの点が、HTMXと組み合わせて使う際にSerializerがやや不自然に感じられる理由かもしれません。


まとめ

前回の記事では、HTMXがデータをどのように送信するかを見てきました。そして今回の記事では、そのデータをDjangoでどのように検証するのが最も自然であるかを整理しました。

  • HTMXは基本的にフォームデータと相性が良いです
  • Django Formはまさにそのフローのために設計されたツールです
  • そのためHTMXとの相性だけを考えるとFormが最も自然です
  • DRF Serializerは使用可能ですが、より戦略的な選択肢に近いと言えます

個人的には、HTMXを適切に活用するには、「AJAXをHTMLらしく扱う感覚」だけでなく、「Django Formとフォームタグを活用する感覚」も同時に取り戻す必要があると考えています。


関連記事