記録を修正・削除する
PUTとDELETEメソッドで勤務記録の修正と削除を実装します。
このレッスンで作るもの
CRUDの残り2つ、Update と Delete を実装します。
PUT /records/:id- 打刻の修正DELETE /records/:id- 記録の削除
日々の仕事に追われて退勤打刻を忘れていた・出勤してるのに打刻してない。みたいなミスは割とよくありますよね。 こういうミスに対応できるように修正・削除のAPIを作りましょう。
Step 1: Partial<AttendanceRecord> を知る
修正APIでは「出勤時刻だけ修正したい」「備考だけ更新したい」のように、一部のプロパティだけを送るのが一般的です。
このとき、の引数の型に AttendanceRecord をそのまま使うと、全プロパティを渡さなければ型エラーになってしまいます。
そこで使うのが Partial<T> です。TypeScript の組み込みユーティリティ型で、全てのプロパティをオプショナル(省略可能)にします。
// Partial<AttendanceRecord> と書くだけで、TypeScript が内部的に以下のように展開してくれる
// 自分で定義する必要はない
{
id?: string;
employeeName?: string;
date?: string;
clockIn?: string;
clockOut?: string | null;
note?: string | null;
}Partial<AttendanceRecord> と書くだけで全プロパティがオプショナルになるため、自分で新しい型を定義する必要はありません。次のステップで、この型を引数に使ったを作ります。
Partial<T> の他にも Required<T>(全プロパティを必須にする)や Pick<T, K>(指定したプロパティだけを取り出す)など、便利なユーティリティ型があります。
Step 2: record-repositoryにを追加する
updateRecord と deleteRecord を src/record-repository.ts に追加します。
// ... 既存の関数 ...
export function updateRecord(
id: string,
updates: Partial<AttendanceRecord>,
): AttendanceRecord | undefined {
const index = records.findIndex((record) => record.id === id);
if (index === -1) return undefined;
records[index] = { ...records[index], ...updates };
return records[index];
}
export function deleteRecord(id: string): boolean {
const index = records.findIndex((record) => record.id === id);
if (index === -1) return false;
records.splice(index, 1);
return true;
}追加したの解説
| 役割 | |
|---|---|
| updateRecord(id, updates) | IDで記録を検索し、渡されたプロパティだけを上書きする |
| deleteRecord(id) | IDで記録を検索し、配列から削除する |
スプレッド構文による部分更新
updateRecord の中で使っている { ...records[index], ...updates } がこののポイントです。
スプレッド構文(...)はオブジェクトの中身を展開する構文で、同じキーがある場合は後に書いた方で上書きされます。
const original = { clockIn: '09:15', clockOut: null, note: '電車遅延' };
const updates = { clockIn: '09:00', note: '遅延証明書提出済み' };
const result = { ...original, ...updates };
// → { clockIn: '09:00', clockOut: null, note: '遅延証明書提出済み' }updates に含まれる clockIn と note だけが上書きされ、clockOut など送っていないプロパティはそのまま残ります。これにより、変更したいプロパティだけを送れば部分更新ができる仕組みです。
Step 3: 修正APIを作る
src/index.ts に PUT /records/:id を追加します。退勤打刻のの下に追加してください。
import express from 'express';
import {
getRecords,
getRecordById,
addRecord,
updateRecord,
deleteRecord,
generateId,
} from './record-repository.js';
// ... 既存のエンドポイント ...
// 記録を修正
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);
});PUTについて
PUT は既存のリソースを更新するためのです。
PUT /records/3 → ID:3 の記録を更新
req.params.id でURLのIDを取得し、req.body で更新したいデータを受け取ります。
Step 4: curlで修正をテストする
田中太郎さん(id: 3)の出勤時刻を修正してみましょう。備考も更新します。
curl -X PUT http://localhost:3000/records/3 \
-H "Content-Type: application/json" \
-d '{"clockIn": "09:00", "note": "遅延証明書提出済み"}'成功すると更新後のデータが返ってきます。
{
"id": "3",
"employeeName": "田中太郎",
"date": "2026-01-28",
"clockIn": "09:00",
"clockOut": null,
"note": "遅延証明書提出済み"
}clockIn が "09:15" から "09:00" に、note が "電車遅延" から "遅延証明書提出済み" に更新されていることを確認しましょう。
送ったプロパティだけが更新され、employeeName や date はそのまま残っています。
これが Step 2 で解説したスプレッド構文による部分更新の効果です。
Step 5: 削除APIを作る
削除用(DELETE /records/:id)をsrc/index.tsに追加します。
// 記録を削除
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();
});204
204 No Content は「処理は成功したが、返すデータはない」を意味します。
削除が成功した場合 204 を使います。
削除は常に成功するとは限りません。例えば、データベースを使っていてデータベースの接続に失敗して削除ができない場合は500 Internal Server Errorなどを返します。
204を見ると、削除に成功したことが一目で判断できます。
Step 6: curlで削除をテストする
佐藤花子さん(id: 2)の記録を削除してみましょう。
curl -X DELETE http://localhost:3000/records/2 -v-v はの詳細(やヘッダー)を表示するオプションです。204 No Content が返ってくることを確認しましょう。
削除後、一覧を取得して確認します。
curl http://localhost:3000/records佐藤花子の記録が消えていれば成功です。
存在しないIDの削除
存在しないIDを指定した場合のエラーも確認しておきましょう。
curl -X DELETE http://localhost:3000/records/999{
"error": "記録が見つかりません"
}CRUD完成
ここまでで、全てのCRUD操作が完成しました。
恐らくレッスン1の段階ではイメージが難しかったかもしれませんが、改めての表を見てみましょう。
| 操作 | レッスン | ||
|---|---|---|---|
| Create(作成) | POST | /records/clock-in, /records/clock-out | レッスン5 |
| Read(読取) | GET | /records, /records/:id | レッスン4 |
| Update(更新) | PUT | /records/:id | レッスン6(今回) |
| Delete(削除) | DELETE | /records/:id | レッスン6(今回) |
『あぁ、なるほどな』と少しでも分かってもらえたら嬉しいです。
完成コード
import { AttendanceRecord } from './types.js';
import { records } from './data.js';
let nextId = records.length + 1;
export function getRecords(): AttendanceRecord[] {
return records;
}
export function getRecordById(id: string): AttendanceRecord | undefined {
return records.find((record) => record.id === id);
}
export function generateId(): string {
const id = String(nextId);
nextId++;
return id;
}
export function addRecord(record: AttendanceRecord): void {
records.push(record);
}
export function updateRecord(
id: string,
updates: Partial<AttendanceRecord>,
): AttendanceRecord | undefined {
const index = records.findIndex((record) => record.id === id);
if (index === -1) return undefined;
records[index] = { ...records[index], ...updates };
return records[index];
}
export function deleteRecord(id: string): boolean {
const index = records.findIndex((record) => record.id === id);
if (index === -1) return false;
records.splice(index, 1);
return true;
}import express from 'express';
import {
getRecords,
getRecordById,
addRecord,
updateRecord,
deleteRecord,
generateId,
} from './record-repository.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;
}
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;
}
const record = getRecordById(id);
if (!record) {
res.status(404).json({ error: '記録が見つかりません' });
return;
}
if (record.clockOut !== null) {
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}`);
});次のステップ
勤怠管理APIが全て揃いました。しかし、現在のAPIにはまだ問題があります。
例えば、出勤時刻に "abc" のような不正な値を送っても受け付けてしまいます。
次のレッスンでは、バリデーション(入力値の検証)を追加して、より堅牢なAPIに仕上げます。