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?

Request/Responseだけで作る練習フォーム:入力検証とHTML出力を分ける

0
Posted at

短い文章と練習時間を入力すると、その場で練習手順を返す小さなフォームを題材に、入力検証とHTML出力の境界を整理します。フレームワークやデータベースを用意しなくても、Web標準の Request と Response で処理を一つの関数にまとめられます。

例として使うのは、私たちのチームで公開した 練習計画フォームのコード です。Pronunciation Coach の関連サンプルとして、既にある文章とフィードバックから練習手順を組み立てる部分を独立させています。製品本体のコードや音声処理は含めていません。

前提と処理の流れ

ローカルの確認環境は Node.js v24.15.0 です。公開コードはWeb標準のHTTPハンドラー形式になっており、以下の確認では関数へ直接 Request を渡します。実際のホスティング先での稼働確認とは分けて考えます。

流れは GET → フォーム表示 と POST → 入力検証 → 手順の生成 → HTMLレスポンス の二つです。アプリコードに外部APIの呼び出しや入力の永続保存処理はありません。ホスティング基盤側のログまで扱う説明ではありません。

1. 選択肢はサーバー側でも限定する

ブラウザーの select を使っても、リクエストの値まで保証されるわけではありません。サーバー側では、受け付ける値を明示します。

const form = await request.formData();
const sentence = String(form.get("sentence") || "").trim();
const feedback = String(form.get("feedback") || "").trim();
const minutes = String(form.get("minutes") || "3");

const valid = sentence.length > 0
  && sentence.length <= 240
  && feedback.length <= 480
  && ["1", "3", "5"].includes(minutes);

この例の長さは String.length、つまりUTF-16コード単位で数えます。見た目の文字数を数える処理ではありません。上限の意味をUIと検証コードで揃えておくと、絵文字などを入力したときの解釈も曖昧になりません。

公開コードには Content-Length を見て4KBを超える申告を早期に返す処理もあります。ただし、このヘッダーの確認だけで実際の受信量を保証できるわけではありません。大きな入力を確実に制限する場合は、実行基盤のリクエスト上限や読み取り方法も別に設計します。

2. 判定と表示のエスケープは別の処理にする

「240文字以内」と「HTMLとして解釈されない」は別の条件です。検証を通った文章にも < や & は含まれるため、HTMLへ埋め込む場所でエスケープします。

const escape = value => String(value).replace(/[&<>"']/g, c => ({
  "&": "&amp;", "<": "&lt;", ">": "&gt;",
  '"': "&quot;", "'": "&#39;"
}[c]));

const paragraph = "<p>" + escape(sentence) + "</p>";

ここで扱うのはHTMLのテキスト位置です。同じ関数を、そのままJavaScript、CSS、任意のURLなどの文脈へ流用する説明ではありません。入力した値をフォームへ戻す箇所にも、同じ出力上の注意が必要です。

3. 自由入力から判断を増やしすぎない

練習時間は次の固定テーブルから選びます。

const blocks = {
  "1": [10, 15, 20, 15],
  "3": [30, 45, 60, 45],
  "5": [50, 75, 100, 75]
}[minutes];

四つの数値は、最初の試行、手がかりの確認、反復、比較に使う秒数です。合計はそれぞれ60秒、180秒、300秒になります。フィードバックが空のときは一般的な練習手順を返し、入力されていない弱点や採点結果を作りません。小さな補助ツールでは、このように「入力から何を決めるか」を絞ると挙動を追いやすくなります。

4. ネットワークを使わずHTTPの境界を確認する

元のハンドラーを practice-loop.mjs として保存した場合、次のように関数へ直接リクエストを渡せます。

import assert from "node:assert/strict";
import practiceLoop from "./practice-loop.mjs";

const form = new URLSearchParams({
  sentence: "Could I have a coffee, please?",
  feedback: "Focus on one word",
  minutes: "3"
});
const response = await practiceLoop(new Request("https://example.test/", {
  method: "POST", body: form
}));
const html = await response.text();
assert.match(html, /Your 3-minute practice loop/);
assert.match(html, /First attempt — 30 seconds/);
assert.match(html, /Repeat — 60 seconds/);

このほか、GETでフォームが返ること、1・3・5分の各分岐、空文と上限超過と不正な時間のエラー表示、タグのような文字列のエスケープ、未対応メソッドの405応答をローカルで確認しました。公開ホストへアクセスできることや、ブラウザー上でフォームが操作できることまで、この確認だけで証明したことにはしません。

おわりに

小さなフォームでも、入力の条件、生成する結果、HTMLへ戻す値を分けると、確認すべき境界が見えやすくなります。まずは一つのハンドラーで挙動を固定し、認証や保存が必要になった時点で、その責務を追加する形にすると見通しを保ちやすくなります。

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?