tanaka101

記録を修正・削除する

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にを追加する

updateRecorddeleteRecordsrc/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 に含まれる clockInnote だけが上書きされ、clockOut など送っていないプロパティはそのまま残ります。これにより、変更したいプロパティだけを送れば部分更新ができる仕組みです。

Step 3: 修正APIを作る

src/index.tsPUT /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"電車遅延" から "遅延証明書提出済み" に更新されていることを確認しましょう。

送ったプロパティだけが更新され、employeeNamedate はそのまま残っています。
これが 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(今回)

『あぁ、なるほどな』と少しでも分かってもらえたら嬉しいです。

完成コード

src/record-repository.ts
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;
}
src/index.ts
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に仕上げます。