tanaka101

出勤・退勤を記録する

POSTリクエストで出勤・退勤の打刻を記録するAPIを作ります。

このレッスンで作るもの

POSTを使って、出勤と退勤を記録するAPIを2つ作ります。

  • POST /records/clock-in - 出勤打刻
  • POST /records/clock-out - 退勤打刻

GETとPOSTの違い

前のレッスンでは GET でデータを取得しました。今回は POST でデータを作成・更新します。

用途データの送り方
GETデータを取得するURLのみ(ボディなし)
POSTデータを作成するリクエストボディにJSONを含める

なぜ打刻にPOSTを使うのか

POSTは新規登録で使われることが多いですが、実は 「GETやPUT、DELETEに当てはまらない操作全般」 に使える万能なです。

出勤打刻は新しい勤務記録を1件作成するので、POSTの典型的な使い方です。 一方、退勤打刻は既存の記録に clockOut を書き込む「更新」ですが、これもPOSTで実装しています。 更新ならPUT(次のレッスンで学びます)の方が適切に思えるかもしれません。POSTを使う理由は (べきとうせい) にあります。

GET冪等(何回実行しても同じ結果)記録の一覧を取得 → 何度取得しても同じ
PUT冪等(何回実行しても同じ結果)名前を「田中」に更新 → 何度やっても「田中」
POST冪等でない(実行するたびに結果が変わりうる)出勤打刻 → 送るたびに記録が増える

PUTは「同じを何度送っても結果が同じ」になることが期待されるです。しかし打刻は本来、同じ日に何度も出勤・退勤できるべきではありません。実行するたびに結果が変わりうる(または拒否すべき)操作なので、冪等でないPOSTが適しています。

POSTを選ぶもう一つの理由 ― ブラウザやインフラとの連携

の使い分けは、サーバーの実装だけでなくブラウザやネットワーク機器の動作にも影響します。

たとえば、ブラウザでPOSTを送信した後に「戻る」ボタンや「更新」ボタンを押すと、「フォームを再送信しますか?」という確認ダイアログが表示されます。これはPOSTが「冪等でない=もう一度実行すると結果が変わるかもしれない」だとブラウザが知っているからです。

一方、GETやPUTにはこの確認が出ません。「何度実行しても安全」だとブラウザが判断するためです。

POST → ブラウザ「本当にもう一度送りますか?」(確認ダイアログ)
GET  → ブラウザ「そのまま再取得します」(確認なし)

つまり、を正しく使い分けることで、ブラウザやプロキシといった周辺のインフラが適切に振る舞ってくれるのです。これがREST APIでの使い分けが重要とされる大きな理由の一つです。

Step 1: データの配列を書き換え可能にする

前のレッスンでは data.tsconst で定義していました。今回はPOSTで新しい記録を追加(push)するため、配列を let に変更する必要があります。

src/data.ts を以下のように修正してください。

src/data.ts
import { AttendanceRecord } from './types.js';
 
export let records: AttendanceRecord[] = [
  {
    id: '1',
    employeeName: '田中太郎',
    date: '2026-01-27',
    clockIn: '09:00',
    clockOut: '18:00',
    note: null,
  },
  {
    id: '2',
    employeeName: '佐藤花子',
    date: '2026-01-27',
    clockIn: '08:45',
    clockOut: '17:30',
    note: null,
  },
  {
    id: '3',
    employeeName: '田中太郎',
    date: '2026-01-28',
    clockIn: '09:15',
    clockOut: null,
    note: '電車遅延',
  },
];

変更点は export const recordsexport let records の1箇所だけです。

なぜ let に変更するのか?

const で宣言した配列に push でデータを追加すること自体は実はJavaScriptでは可能です。const は「変数への再代入」を禁止するだけで、配列の中身の変更は許可されるためです。

const arr = [1, 2, 3];
arr.push(4);     // OK(中身の変更)
arr = [5, 6, 7]; // エラー(再代入は不可)

しかし、const は「この値は変わらない」という意図を示すために使うのが一般的です。今回のように中身が変わる配列を const で宣言すると、コードを読む人に誤解を与えてしまいます。変更されることが前提のデータは let で宣言しましょう。

Step 2: record-repositoryにPOST用のを追加する

前のレッスンでは record-repository.tsgetRecordsgetRecordById を定義しました。
ここに、出勤打刻で使う addRecordgenerateId を追加します。

src/record-repository.ts を以下のように修正してください。

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);
}

追加したの解説

役割
generateId()連番でIDを採番する(4, 5, 6...)
addRecord(record)配列に新しい記録を追加する

nextIdrecords.length + 1 で初期化しています。が3件あるので、最初の generateId()"4" を返します。

本来、IDの採番はデータベースが自動で行います。ここではを使っているため、簡易的に連番で管理しています。

Step 3: 出勤打刻APIを作る

src/index.ts に出勤打刻のを追加します。

src/index.ts
import express from 'express';
import { getRecords, getRecordById, addRecord, 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.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

コードの解説

const { employeeName, date, clockIn, note } = req.body;

リクエストボディからデータを取り出しています。これは 分割代入(destructuring) という書き方です。

分割代入は、オブジェクトのプロパティを個別の変数に展開する構文です。

// 通常の書き方
const employeeName = req.body.employeeName;
const date = req.body.date;
 
// 分割代入(同じ意味)
const { employeeName, date } = req.body;

コードが短くなり、読みやすくなります。

res.status(201).json(newRecord);

201(Created)は「新しいリソースが作成された」ことを意味します。POSTで新しいデータを作成した場合に使います。

改ざんの可能性のある処理はサーバー側で行いましょう

このレッスンではリクエストボディで clockInclockOut の時刻を送っていますが、これは学習用の簡易的な実装です。 実際の勤怠システムでは、クライアントから送られた時刻をそのまま信用するのは危険です。 ユーザーが開発者ツールなどで時刻を改ざんできてしまうためです。

本番環境ではサーバーが受信した時点の時刻(new Date())を使って記録するのが一般的です。

Step 4: curlで出勤打刻をテストする

GETはブラウザのアドレスバーからテストできましたが、POSTはブラウザから直接送ることができません。から curl コマンドを使ってテストします。

curl(カール)からを送れるコマンドラインツールです。

APIの動作確認でよく使われます。ほとんどのOS(Mac / Linux / Windows 10以降)にプリインストールされています。

開発サーバーが起動している状態で、別のを開いて以下のコマンドを実行します。

curl -X POST http://localhost:3000/records/clock-in \
-H "Content-Type: application/json" \
-d '{"employeeName": "鈴木一郎", "date": "2026-01-28", "clockIn": "09:00"}'

curlコマンドの解説

オプション意味
-X POSTPOSTを送る
-H "Content-Type: application/json"送るデータがJSON形式であることを伝える
-d '{...}'リクエストボディ(送るデータ)

POSTに成功すると以下のようなが返ります。

{
  "id": "4",
  "employeeName": "鈴木一郎",
  "date": "2026-01-28",
  "clockIn": "09:00",
  "clockOut": null,
  "note": null
}

curlで一覧を取得して、新しい記録が追加されていることを確認しましょう。

curl http://localhost:3000/records

追加したデータは メモリ上の配列 に保存されているだけなので、サーバーを再起動すると元の3件に戻ります。 data.ts ファイル自体も書き換わりません。

実際のアプリケーションではデータベースを使ってデータを永続化します。

Step 5: 退勤打刻APIを作る

退勤打刻は、既存の出勤記録に対して clockOut を更新する処理です。

出勤打刻のの下に追加してください。

src/index.ts
// 退勤打刻
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);
});

ポイント

退勤打刻では3つのチェックを行っています。

  1. 必須項目チェック - idclockOut が必要
  2. 存在チェック - 指定されたIDの記録が存在するか
  3. 二重打刻チェック - すでに退勤が記録されていないか

Step 6: curlで退勤打刻をテストする

先ほど出勤登録した鈴木一郎さん(id: 4)の退勤を記録します。

curl -X POST http://localhost:3000/records/clock-out \
-H "Content-Type: application/json" \
-d '{"id": "4", "clockOut": "18:00"}'

成功すると以下のようなが返ります。

{
  "id": "4",
  "employeeName": "鈴木一郎",
  "date": "2026-01-28",
  "clockIn": "09:00",
  "clockOut": "18:00",
  "note": null
}

エラーケースも試してみよう

すでに退勤済みの記録に対してもう一度退勤を記録しようとするとどうなるか試してみましょう。

curl -X POST http://localhost:3000/records/clock-out \
-H "Content-Type: application/json" \
-d '{"id": "4", "clockOut": "19:00"}'
{
  "error": "すでに退勤が記録されています"
}

エラーが正しく返ってきますね。

完成コード

src/data.ts
import { AttendanceRecord } from './types.js';
 
export let records: AttendanceRecord[] = [
  {
    id: '1',
    employeeName: '田中太郎',
    date: '2026-01-27',
    clockIn: '09:00',
    clockOut: '18:00',
    note: null,
  },
  {
    id: '2',
    employeeName: '佐藤花子',
    date: '2026-01-27',
    clockIn: '08:45',
    clockOut: '17:30',
    note: null,
  },
  {
    id: '3',
    employeeName: '田中太郎',
    date: '2026-01-28',
    clockIn: '09:15',
    clockOut: null,
    note: '電車遅延',
  },
];
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);
}
src/index.ts
import express from 'express';
import { getRecords, getRecordById, addRecord, 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.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

次のステップ

出勤・退勤の記録ができるようになりました。CRUDのCreateとRead が完成しています。

次のレッスンでは、PUTとDELETEを使って記録の修正と削除(Update と Delete)を実装します。