Node.js レッスン6
Node.js総合演習
REST APIサーバーを作ろう -- タスク管理APIの構築
プロジェクト概要
これまでに学んだNode.js、Express、REST API設計、ミドルウェアの知識を総動員して、タスク管理REST APIを構築します。 以下の機能を持つサーバーを作りましょう。
機能一覧
- • タスクのCRUD操作(作成・取得・更新・削除)
- • ステータスによるフィルタリング
- • バリデーション
- • エラーハンドリング
使用する技術
- • Express(Webフレームワーク)
- • express.Router(ルート分割)
- • ミドルウェア(ログ・CORS・エラー処理)
- • バリデーション関数
Step 1: プロジェクトセットアップ
まず、プロジェクトのディレクトリ構造を作成し、必要なパッケージをインストールします。
# プロジェクト作成
$ mkdir task-api && cd task-api
$ npm init -y
$ npm install express cors
$ npm install -D nodemon
# ディレクトリ構造
# task-api/
# ├── package.json
# ├── server.js ← エントリーポイント
# ├── routes/
# │ └── tasks.js ← タスクルーター
# ├── middleware/
# │ ├── logger.js ← ログミドルウェア
# │ ├── validator.js ← バリデーション
# │ └── errorHandler.js← エラーハンドラ
# └── data/
# └── tasks.js ← データ(仮のDB)package.jsonにスクリプトを追加します。
{
"name": "task-api",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}Step 2: データとミドルウェアの作成
データ管理モジュールとミドルウェアを作成します。
// === data/tasks.js ===
let tasks = [
{
id: 1,
title: 'Express を学ぶ',
description: 'ルーティングとミドルウェアを理解する',
status: 'done',
createdAt: '2026-03-01T10:00:00.000Z',
},
{
id: 2,
title: 'REST API を設計する',
description: 'CRUD操作を実装する',
status: 'in-progress',
createdAt: '2026-03-15T10:00:00.000Z',
},
{
id: 3,
title: 'データベースを接続する',
description: 'PostgreSQL と Prisma を使う',
status: 'todo',
createdAt: '2026-03-29T10:00:00.000Z',
},
];
let nextId = 4;
module.exports = {
getAll: () => tasks,
getById: (id) => tasks.find((t) => t.id === id),
create: (data) => {
const task = {
id: nextId++,
...data,
status: data.status || 'todo',
createdAt: new Date().toISOString(),
};
tasks.push(task);
return task;
},
update: (id, data) => {
const index = tasks.findIndex((t) => t.id === id);
if (index === -1) return null;
tasks[index] = { ...tasks[index], ...data };
return tasks[index];
},
remove: (id) => {
const index = tasks.findIndex((t) => t.id === id);
if (index === -1) return false;
tasks.splice(index, 1);
return true;
},
};// === middleware/logger.js ===
function logger(req, res, next) {
const start = Date.now();
const timestamp = new Date().toISOString();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${timestamp}] ${req.method} ${req.originalUrl} ` +
`→ ${res.statusCode} (${duration}ms)`
);
});
next();
}
module.exports = logger;
// === middleware/validator.js ===
function validateTask(req, res, next) {
const { title } = req.body;
const errors = [];
if (!title || typeof title !== 'string' || title.trim() === '') {
errors.push('title は空でない文字列で必須です');
}
if (req.body.status) {
const validStatuses = ['todo', 'in-progress', 'done'];
if (!validStatuses.includes(req.body.status)) {
errors.push(
`status は ${validStatuses.join(', ')} のいずれかです`
);
}
}
if (errors.length > 0) {
return res.status(422).json({ errors });
}
next();
}
module.exports = { validateTask };
// === middleware/errorHandler.js ===
class AppError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
}
}
function notFoundHandler(req, res, next) {
next(new AppError(404, `${req.method} ${req.url} は存在しません`));
}
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
const message = err.message || 'サーバー内部エラー';
console.error(`[ERROR] ${statusCode}: ${message}`);
res.status(statusCode).json({
error: {
code: statusCode,
message,
},
});
}
module.exports = { AppError, notFoundHandler, errorHandler };Step 3: タスクルーターの作成
タスクのCRUD操作をルーターとして実装します。
// === routes/tasks.js ===
const express = require('express');
const router = express.Router();
const taskDB = require('../data/tasks');
const { validateTask } = require('../middleware/validator');
const { AppError } = require('../middleware/errorHandler');
// GET /api/tasks - タスク一覧(フィルタリング対応)
router.get('/', (req, res) => {
let tasks = taskDB.getAll();
// ステータスでフィルタリング
if (req.query.status) {
tasks = tasks.filter((t) => t.status === req.query.status);
}
// 検索キーワード
if (req.query.q) {
const q = req.query.q.toLowerCase();
tasks = tasks.filter(
(t) =>
t.title.toLowerCase().includes(q) ||
(t.description && t.description.toLowerCase().includes(q))
);
}
res.json({
data: tasks,
count: tasks.length,
});
});
// GET /api/tasks/:id - タスク詳細
router.get('/:id', (req, res, next) => {
const task = taskDB.getById(Number(req.params.id));
if (!task) {
return next(new AppError(404, 'タスクが見つかりません'));
}
res.json(task);
});
// POST /api/tasks - タスク作成
router.post('/', validateTask, (req, res) => {
const { title, description, status } = req.body;
const newTask = taskDB.create({
title: title.trim(),
description: description?.trim() || '',
status,
});
res.status(201).json(newTask);
});
// PUT /api/tasks/:id - タスク更新
router.put('/:id', validateTask, (req, res, next) => {
const { title, description, status } = req.body;
const updated = taskDB.update(Number(req.params.id), {
title: title.trim(),
description: description?.trim() || '',
status,
});
if (!updated) {
return next(new AppError(404, 'タスクが見つかりません'));
}
res.json(updated);
});
// PATCH /api/tasks/:id/status - ステータスのみ更新
router.patch('/:id/status', (req, res, next) => {
const { status } = req.body;
const validStatuses = ['todo', 'in-progress', 'done'];
if (!validStatuses.includes(status)) {
return res.status(422).json({
errors: [`status は ${validStatuses.join(', ')} のいずれかです`],
});
}
const updated = taskDB.update(Number(req.params.id), { status });
if (!updated) {
return next(new AppError(404, 'タスクが見つかりません'));
}
res.json(updated);
});
// DELETE /api/tasks/:id - タスク削除
router.delete('/:id', (req, res, next) => {
const deleted = taskDB.remove(Number(req.params.id));
if (!deleted) {
return next(new AppError(404, 'タスクが見つかりません'));
}
res.status(204).send();
});
module.exports = router;Step 4: サーバー本体の作成
すべてのパーツを組み合わせて、サーバーを完成させます。
// === server.js ===
const express = require('express');
const cors = require('cors');
const logger = require('./middleware/logger');
const { notFoundHandler, errorHandler } = require('./middleware/errorHandler');
const tasksRouter = require('./routes/tasks');
const app = express();
const PORT = process.env.PORT || 3000;
// === ミドルウェア ===
app.use(cors());
app.use(logger);
app.use(express.json());
// === ルーティング ===
// ヘルスチェック
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
// タスクAPI
app.use('/api/tasks', tasksRouter);
// === エラーハンドリング ===
app.use(notFoundHandler);
app.use(errorHandler);
// === サーバー起動 ===
app.listen(PORT, () => {
console.log(`タスク管理APIサーバー起動: http://localhost:${PORT}`);
console.log('利用可能なエンドポイント:');
console.log(' GET /api/health');
console.log(' GET /api/tasks');
console.log(' GET /api/tasks/:id');
console.log(' POST /api/tasks');
console.log(' PUT /api/tasks/:id');
console.log(' PATCH /api/tasks/:id/status');
console.log(' DELETE /api/tasks/:id');
});Step 5: APIをテストする
curlコマンドやHTTPクライアントを使って、作成したAPIをテストしましょう。
# サーバー起動
$ npm run dev
# === curlでテスト ===
# ヘルスチェック
$ curl http://localhost:3000/api/health
# → {"status":"ok","timestamp":"2026-03-29T..."}
# タスク一覧取得
$ curl http://localhost:3000/api/tasks
# → {"data":[...],"count":3}
# ステータスでフィルタリング
$ curl "http://localhost:3000/api/tasks?status=todo"
# キーワード検索
$ curl "http://localhost:3000/api/tasks?q=Express"
# タスク詳細取得
$ curl http://localhost:3000/api/tasks/1
# タスク作成
$ curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"テストを書く","description":"Jestでユニットテスト"}'
# → {"id":4,"title":"テストを書く",...,"status":"todo"}
# タスク更新
$ curl -X PUT http://localhost:3000/api/tasks/4 \
-H "Content-Type: application/json" \
-d '{"title":"テストを書く(更新)","status":"in-progress"}'
# ステータスのみ更新
$ curl -X PATCH http://localhost:3000/api/tasks/4/status \
-H "Content-Type: application/json" \
-d '{"status":"done"}'
# タスク削除
$ curl -X DELETE http://localhost:3000/api/tasks/4
# → (204 No Content)
# 存在しないタスク
$ curl http://localhost:3000/api/tasks/999
# → {"error":{"code":404,"message":"タスクが見つかりません"}}
# バリデーションエラー
$ curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":""}'
# → {"errors":["title は空でない文字列で必須です"]}まとめ
- Express + Router でルーティングを整理し、関心ごとにファイルを分割した
- ログ、バリデーション、エラーハンドリングのミドルウェアを実装した
- CRUD操作(GET/POST/PUT/PATCH/DELETE)でタスクを管理するAPIを構築した
- クエリパラメータによるフィルタリング・検索機能を追加した
- 統一されたエラーレスポンス形式で、クライアントにわかりやすいエラー情報を返した
- curlコマンドでAPIのテストを行い、各エンドポイントの動作を確認した