tanaka101

エラーを返す

バリデーションを追加して、不正なリクエストに適切なエラーを返します。

このレッスンで学ぶこと

現在のAPIは、不正なデータを送っても受け付けてしまいます。例えば:

  • 出勤時刻に "abc" を送れる
  • 日付に "昨日" を送れる
  • 退勤時刻が出勤時刻より前でも通ってしまう

このレッスンでは バリデーション(入力値の検証) を追加して、不正なにはエラーを返すようにします。

バリデーションとは

APIが受け取るデータが「正しい形式か」を検証する処理です。

クライアント(Webブラウザなど) → リクエスト → [バリデーション] → 処理 → レスポンス
                                                ↓
                                            不正なら 400 エラー

システムやサービスにおいてバリデーションは非常に重要です。
不正なデータがシステムに入り込むと、予期しないエラーの原因になります。

Step 1: バリデーションを作る

src/validation.ts を作成して、バリデーションをまとめます。

src/validation.ts
// 日付の形式チェック(YYYY-MM-DD)
export function isValidDate(date: string): boolean {
  const regex = /^\d{4}-\d{2}-\d{2}$/;
  if (!regex.test(date)) return false;
 
  const parsed = new Date(date);
  return !isNaN(parsed.getTime());
}
 
// 時刻の形式チェック(HH:mm)
export function isValidTime(time: string): boolean {
  const regex = /^\d{2}:\d{2}$/;
  if (!regex.test(time)) return false;
 
  const [hours, minutes] = time.split(':').map(Number);
  return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
}
 
// 退勤時刻が出勤時刻より後かチェック
export function isClockOutAfterClockIn(clockIn: string, clockOut: string): boolean {
  return clockOut > clockIn;
}

コードの解説

正規表現

/^\d{4}-\d{2}-\d{2}$/ は日付形式をチェックする正規表現です。

パターン意味
^文字列の先頭
\d{4}数字4桁(年)
-ハイフン
\d{2}数字2桁(月・日)
$文字列の末尾

正規表現(Regular Expression) は文字列のパターンを表現するための記法です。

regex.test(date) は、date が正規表現のパターンに一致するかを true / false で返します。

正規表現の使用例
const regex = /^\d{4}-\d{2}-\d{2}$/;
 
regex.test('2026-01-28');  // true
regex.test('abc');          // false
regex.test('2026/01/28');   // false(ハイフンではなくスラッシュ)

正規表現は奥が深いですが、今回は「形式が合っているか」のチェックに使っています。
暗記する必要はなく、必要なときに調べてコピペすれば大丈夫です。

日付の妥当性チェック

正規表現だけでは 2026-99-99 のような存在しない日付も通ってしまいます。

new Date(date) で実際にDateオブジェクトに変換し、isNaN(parsed.getTime()) で有効な日付かを確認しています。

new Date('2026-01-28').getTime();  // 1769472000000(有効)
new Date('2026-99-99').getTime();  // NaN(無効)

実務では Zod などの バリデーションライブラリを使うことが多いです。
ただ、自力で書いてみると「何をチェックすべきか」の感覚が身につくのでいい勉強になります。

Step 2: 出勤打刻にバリデーションを追加する

src/index.ts の出勤打刻を修正します。

src/index.ts(import文の変更 + 出勤打刻バリデーション追加)
import express from 'express';
import {
  getRecords,
  getRecordById,
  addRecord,
  updateRecord,
  deleteRecord,
  generateId,
} from './record-repository.js';
import { isValidDate, isValidTime } from './validation.js';
 
// ... 既存のコード ...
 
// 出勤打刻
app.post('/records/clock-in', (req, res) => {
  const { employeeName, date, clockIn, note } = req.body;
 
  if (!employeeName || !date || !clockIn) {
    res.status(400).json({ error: 'employeeName, date, clockIn は必須です' });
    return;
  }
 
  if (!isValidDate(date)) {
    res.status(400).json({ error: '日付の形式が正しくありません(例: 2026-01-28)' });
    return;
  }
 
  if (!isValidTime(clockIn)) {
    res.status(400).json({ error: '時刻の形式が正しくありません(例: 09:00)' });
    return;
  }
 
  const newRecord = {
    id: generateId(),
    employeeName,
    date,
    clockIn,
    clockOut: null,
    note: note || null,
  };
 
  addRecord(newRecord);
  res.status(201).json(newRecord);
});

Step 3: 退勤打刻にバリデーションを追加する

退勤打刻にも時刻のバリデーションと、出勤より後かのチェックを追加します。

src/index.ts(import文の変更 + 退勤打刻バリデーション追加)
import { isValidDate, isValidTime, isClockOutAfterClockIn } from './validation.js';
 
// ... 既存のコード ...
 
// 退勤打刻
app.post('/records/clock-out', (req, res) => {
  const { id, clockOut } = req.body;
 
  if (!id || !clockOut) {
    res.status(400).json({ error: 'id, clockOut は必須です' });
    return;
  }
 
  if (!isValidTime(clockOut)) {
    res.status(400).json({ error: '時刻の形式が正しくありません(例: 18:00)' });
    return;
  }
 
  const record = getRecordById(id);
 
  if (!record) {
    res.status(404).json({ error: '記録が見つかりません' });
    return;
  }
 
  if (record.clockOut !== null) {
    res.status(400).json({ error: 'すでに退勤が記録されています' });
    return;
  }
 
  if (!isClockOutAfterClockIn(record.clockIn, clockOut)) {
    res.status(400).json({ error: '退勤時刻は出勤時刻より後にしてください' });
    return;
  }
 
  record.clockOut = clockOut;
  res.json(record);
});

Step 4: バリデーションの動作確認

不正なデータでを送って、エラーが返ることを確認しましょう。

不正な日付

curl -X POST http://localhost:3000/records/clock-in \
-H "Content-Type: application/json" \
-d '{"employeeName": "テスト", "date": "abc", "clockIn": "09:00"}'
{
  "error": "日付の形式が正しくありません(例: 2026-01-28)"
}

不正な時刻

curl -X POST http://localhost:3000/records/clock-in \
-H "Content-Type: application/json" \
-d '{"employeeName": "テスト", "date": "2026-01-28", "clockIn": "25:00"}'
{
  "error": "時刻の形式が正しくありません(例: 09:00)"
}

退勤が出勤より前

まず正常に出勤登録して、そのIDに対して出勤より前の時刻で退勤を送ります。

curl -X POST http://localhost:3000/records/clock-out \
-H "Content-Type: application/json" \
-d '{"id": "3", "clockOut": "08:00"}'
{
  "error": "退勤時刻は出勤時刻より後にしてください"
}

完成コード

src/validation.ts 全体
// 日付の形式チェック(YYYY-MM-DD)
export function isValidDate(date: string): boolean {
  const regex = /^\d{4}-\d{2}-\d{2}$/;
  if (!regex.test(date)) return false;
 
  const parsed = new Date(date);
  return !isNaN(parsed.getTime());
}
 
// 時刻の形式チェック(HH:mm)
export function isValidTime(time: string): boolean {
  const regex = /^\d{2}:\d{2}$/;
  if (!regex.test(time)) return false;
 
  const [hours, minutes] = time.split(':').map(Number);
  return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
}
 
// 退勤時刻が出勤時刻より後かチェック
export function isClockOutAfterClockIn(clockIn: string, clockOut: string): boolean {
  return clockOut > clockIn;
}
src/index.ts 全体
import express from 'express';
import {
  getRecords,
  getRecordById,
  addRecord,
  updateRecord,
  deleteRecord,
  generateId,
} from './record-repository.js';
import { isValidDate, isValidTime, isClockOutAfterClockIn } from './validation.js';
 
const app = express();
 
app.use(express.json());
 
const PORT = 3000;
 
// 勤務記録の一覧を取得
app.get('/records', (req, res) => {
  const records = getRecords();
  res.json(records);
});
 
// 特定の勤務記録を取得
app.get('/records/:id', (req, res) => {
  const record = getRecordById(req.params.id);
 
  if (!record) {
    res.status(404).json({ error: '記録が見つかりません' });
    return;
  }
 
  res.json(record);
});
 
// 出勤打刻
app.post('/records/clock-in', (req, res) => {
  const { employeeName, date, clockIn, note } = req.body;
 
  if (!employeeName || !date || !clockIn) {
    res.status(400).json({ error: 'employeeName, date, clockIn は必須です' });
    return;
  }
 
  if (!isValidDate(date)) {
    res.status(400).json({ error: '日付の形式が正しくありません(例: 2026-01-28)' });
    return;
  }
 
  if (!isValidTime(clockIn)) {
    res.status(400).json({ error: '時刻の形式が正しくありません(例: 09:00)' });
    return;
  }
 
  const newRecord = {
    id: generateId(),
    employeeName,
    date,
    clockIn,
    clockOut: null,
    note: note || null,
  };
 
  addRecord(newRecord);
  res.status(201).json(newRecord);
});
 
// 退勤打刻
app.post('/records/clock-out', (req, res) => {
  const { id, clockOut } = req.body;
 
  if (!id || !clockOut) {
    res.status(400).json({ error: 'id, clockOut は必須です' });
    return;
  }
 
  if (!isValidTime(clockOut)) {
    res.status(400).json({ error: '時刻の形式が正しくありません(例: 18:00)' });
    return;
  }
 
  const record = getRecordById(id);
 
  if (!record) {
    res.status(404).json({ error: '記録が見つかりません' });
    return;
  }
 
  if (record.clockOut !== null) {
    res.status(400).json({ error: 'すでに退勤が記録されています' });
    return;
  }
 
  if (!isClockOutAfterClockIn(record.clockIn, clockOut)) {
    res.status(400).json({ error: '退勤時刻は出勤時刻より後にしてください' });
    return;
  }
 
  record.clockOut = clockOut;
  res.json(record);
});
 
// 記録を修正
app.put('/records/:id', (req, res) => {
  const { id } = req.params;
  const updates = req.body;
 
  const updatedRecord = updateRecord(id, updates);
 
  if (!updatedRecord) {
    res.status(404).json({ error: '記録が見つかりません' });
    return;
  }
 
  res.json(updatedRecord);
});
 
// 記録を削除
app.delete('/records/:id', (req, res) => {
  const { id } = req.params;
 
  const deleted = deleteRecord(id);
 
  if (!deleted) {
    res.status(404).json({ error: '記録が見つかりません' });
    return;
  }
 
  res.status(204).send();
});
 
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

現在のフォルダ構造

      • data.ts
      • index.ts← 作成するファイル
      • record-repository.ts
      • types.ts
      • validation.ts
    • package.json
    • package-lock.json
    • tsconfig.json

次のステップ

バリデーションを追加して、APIが不正なデータを弾けるようになりました。

ところで、バリデーションが正しく動いていることをどうやって保証しますか? 毎回curlでテストするのは面倒ですし、見落としもありそうです。

次のレッスンでは、テスト を書いて、コードが正しく動くことを自動で検証する方法を学びます。