エラーを返す
バリデーションを追加して、不正なリクエストに適切なエラーを返します。
このレッスンで学ぶこと
現在のAPIは、不正なデータを送っても受け付けてしまいます。例えば:
- 出勤時刻に
"abc"を送れる - 日付に
"昨日"を送れる - 退勤時刻が出勤時刻より前でも通ってしまう
このレッスンでは バリデーション(入力値の検証) を追加して、不正なにはエラーを返すようにします。
バリデーションとは
APIが受け取るデータが「正しい形式か」を検証する処理です。
クライアント(Webブラウザなど) → リクエスト → [バリデーション] → 処理 → レスポンス
↓
不正なら 400 エラー
システムやサービスにおいてバリデーションは非常に重要です。
不正なデータがシステムに入り込むと、予期しないエラーの原因になります。
Step 1: バリデーションを作る
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 の出勤打刻を修正します。
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: 退勤打刻にバリデーションを追加する
退勤打刻にも時刻のバリデーションと、出勤より後かのチェックを追加します。
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": "退勤時刻は出勤時刻より後にしてください"
}完成コード
// 日付の形式チェック(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;
}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でテストするのは面倒ですし、見落としもありそうです。
次のレッスンでは、テスト を書いて、コードが正しく動くことを自動で検証する方法を学びます。