短い文章と練習時間を入力すると、その場で練習手順を返す小さなフォームを題材に、入力検証と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 => ({
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'"
}[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へ戻す値を分けると、確認すべき境界が見えやすくなります。まずは一つのハンドラーで挙動を固定し、認証や保存が必要になった時点で、その責務を追加する形にすると見通しを保ちやすくなります。