404 Not Found

404 Not Found


nginx

「実践:RESTful API プロジェクト」

1. プロジェクトの背景

アリスとボブには、オンライン書店向けの書籍管理APIを開発するという課題が与えられた。アリスはルーティングとミドルウェアのアーキテクチャの設計を担当し、ボブはビジネスロジックの記述を担当した。二人は標準化されたレスポンス形式とエラー処理の規約について合意し、プロジェクトのセットアップから完全なCRUD機能の実装に至るまで、わずか2時間でプロジェクト全体を完了させた。

(1) 以下のことを学びます


2. プロジェクトのディレクトリ構造の設計

適切に構成されたディレクトリ階層は、プロジェクトの保守性の基盤となります。コミュニティのベストプラクティスに従い、Aliceは責任範囲に基づいてプロジェクトを階層化しています:

TEXT
book-api/
├── app.js
├── routes/
│   └── books.js
├── controllers/
│   └── bookController.js
├── middleware/
│   ├── validate.js
│   └── errorHandler.js
└── models/
    └── bookModel.js
ディレクトリ/ファイル 担当 説明
app.js エントリファイル Expressのインスタンスを作成し、ルーティングとミドルウェアを紐付ける
routes/ ルーティング定義 対応するコントローラーを指すHTTPメソッドとパスを定義します
controllers/ ビジネスロジック リクエストを処理し、モデルを呼び出し、レスポンスを返す
middleware/ ミドルウェア リクエストの検証やエラー処理などの横断的関心事
models/ データモデル データ格納およびデータ操作のカプセル化

3. APIエンドポイントの設計

業務要件に基づき、ボブはすべてのAPIエンドポイントの一覧を作成しました:

HTTPメソッド パス 関数 成功時のステータスコード 失敗時のステータスコード
取得 /api/books すべての書籍を取得 200
GET /api/books/:id 1冊の書籍を取得 200 404
投稿 /api/books 新刊 201 400
PUT /api/books/:id 書籍を更新 200 404 / 400
削除 /api/books/:id 書籍を削除 200 404

4. 回答の標準形式

アリスは、フロントエンドチームがフィールド名を推測する必要がないよう、一貫したレスポンス形式を採用するよう主張しています:

▶ 例:成功した応答

JAVASCRIPT
{
  "success": true,
  "data": { "id": 1, "title": "Node.js Guide", "author": "Alice" }
}

▶ 例:エラー応答

JAVASCRIPT
{
  "success": false,
  "error": { "code": 404, "message": "Book not found" }
}
フィールド タイプ 説明
success ブール値 リクエストが成功したかどうか
data 任意 成功時に返されるデータ
error.code 番号 エラーステータスコード
error.message 文字列 エラーの説明

5. データモデル層

Bobは、modelsディレクトリ内の配列を使用してデータベースをシミュレートし、すべてのデータ操作をカプセル化しています:

▶ 例:bookModel.js

JAVASCRIPT
const books = [
  { id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }
];

let nextId = 2;

function findAll() {
  return books;
}

function findById(id) {
  return books.find(b => b.id === id);
}

function create(data) {
  const book = { id: nextId++, ...data };
  books.push(book);
  return book;
}

function update(id, data) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return null;
  books[index] = { ...books[index], ...data };
  return books[index];
}

function remove(id) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return false;
  books.splice(index, 1);
  return true;
}

module.exports = { findAll, findById, create, update, remove };

6. リクエスト検証ミドルウェア

Aliceは、無効なリクエストがコントローラーに到達しないようにするため、ミドルウェア層でデータ検証を実装しています:

▶ 例:validate.js

JAVASCRIPT
function validateBook(req, res, next) {
  const { title, author, year } = req.body;
  const errors = [];
  if (!title || typeof title !== "string") {
    errors.push("title is required and must be a string");
  }
  if (!author || typeof author !== "string") {
    errors.push("author is required and must be a string");
  }
  if (year !== undefined && (typeof year !== "number" || year < 0)) {
    errors.push("year must be a non-negative number");
  }
  if (errors.length > 0) {
    return res.status(400).json({
      success: false,
      error: { code: 400, message: errors.join("; ") }
    });
  }
  next();
}

module.exports = { validateBook };

7. エラー処理ミドルウェア

▶ 例:errorHandler.js

JAVASCRIPT
function errorHandler(err, req, res, next) {
  console.error(err.stack);
  const status = err.status || 500;
  res.status(status).json({
    success: false,
    error: { code: status, message: err.message || "Internal Server Error" }
  });
}

function createError(status, message) {
  const err = new Error(message);
  err.status = status;
  return err;
}

module.exports = { errorHandler, createError };

8. コントローラ層

Bobはコントローラー内でビジネスロジックを処理し、モデルを呼び出し、標準化された形式でレスポンスを返します:

▶ 例:bookController.js

JAVASCRIPT
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");

function getAllBooks(req, res) {
  res.json({ success: true, data: Book.findAll() });
}

function getBookById(req, res, next) {
  const book = Book.findById(Number(req.params.id));
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}

function createBook(req, res) {
  const book = Book.create(req.body);
  res.status(201).json({ success: true, data: book });
}

function updateBook(req, res, next) {
  const book = Book.update(Number(req.params.id), req.body);
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}

function deleteBook(req, res, next) {
  const removed = Book.remove(Number(req.params.id));
  if (!removed) return next(createError(404, "Book not found"));
  res.json({ success: true, data: { message: "Book deleted" } });
}

module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };

9. ルートの定義

Aliceはルーティングをコントローラから切り離しています。ルーティングはマッピングのみを担当します:

▶ 例:routes/books.js

JAVASCRIPT
const express = require("express");
const router = express.Router();
const controller = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");

router.get("/", controller.getAllBooks);
router.get("/:id", controller.getBookById);
router.post("/", validateBook, controller.createBook);
router.put("/:id", validateBook, controller.updateBook);
router.delete("/:id", controller.deleteBook);

module.exports = router;

10. エントリーファイル app.js

▶ 例:app.js

JAVASCRIPT
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");

const app = express();
app.use(express.json());

app.use("/api/books", booksRoute);
app.use(errorHandler);

const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

11. プロジェクトアーキテクチャの概要

アリスは、リクエストの流れを明確に示したアーキテクチャ図を描きました:

100%
graph TD
    A[app.js Entrance] --> B[routes/books.js]
    B --> C{Needs verification?}
    C -->|POST/PUT| D[middleware/validate.js]
    C -->|GET/DELETE| E[controllers/bookController.js]
    D --> E
    E --> F[models/bookModel.js]
    F --> E
    E --> G[Standardized Response Format]
    G --> H[Client]
    E -->|Exception| I[middleware/errorHandler.js]
    I --> H

12. APIテスト

ボブは curl を使用して、各エンドポイントを1つずつ検証します:

curl コマンド 説明
curl localhost:3000/api/books すべての書籍を表示
curl localhost:3000/api/books/1 特定の書籍を探す
curl -X POST -H "Content-Type: application/json" -d '{"title":"New Book","author":"Bob","year":2025}' localhost:3000/api/books 新刊
curl -X PUT -H "Content-Type: application/json" -d '{"year":2026}' localhost:3000/api/books/1 書籍の更新
curl -X DELETE localhost:3000/api/books/1 書籍を削除

▶ 例:挿入およびクエリ操作のテスト

BASH
# Add a book
curl -X POST -H "Content-Type: application/json" \
  -d '{"title":"Express in Action","author":"Evan","year":2024}' \
  localhost:3000/api/books
TEXT
{"success":true,"data":{"id":2,"title":"Express in Action","author":"Evan","year":2024}}
BASH
# Search All Books
curl localhost:3000/api/books
TEXT
{"success":true,"data":[{"id":1,"title":"Node.js Guide","author":"Alice","year":2024},{"id":2,"title":"Express in Action","author":"Evan","year":2024}]}

13. 包括的な例:書籍管理APIの完全版

これまでのすべてのモジュールを組み合わせた、プロジェクト全体のコアとなるコードフローは以下の通りです:

JAVASCRIPT
// models/bookModel.js
const books = [{ id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }];
let nextId = 2;
function findAll() { return books; }
function findById(id) { return books.find(b => b.id === id); }
function create(data) {
  const book = { id: nextId++, ...data };
  books.push(book);
  return book;
}
function update(id, data) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return null;
  books[index] = { ...books[index], ...data };
  return books[index];
}
function remove(id) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return false;
  books.splice(index, 1);
  return true;
}
module.exports = { findAll, findById, create, update, remove };
JAVASCRIPT
// middleware/validate.js
function validateBook(req, res, next) {
  const { title, author } = req.body;
  const errors = [];
  if (!title || typeof title !== "string") errors.push("title is required");
  if (!author || typeof author !== "string") errors.push("author is required");
  if (errors.length > 0) {
    return res.status(400).json({
      success: false,
      error: { code: 400, message: errors.join("; ") }
    });
  }
  next();
}
module.exports = { validateBook };
JAVASCRIPT
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  const status = err.status || 500;
  res.status(status).json({
    success: false,
    error: { code: status, message: err.message || "Internal Server Error" }
  });
}
function createError(status, message) {
  const err = new Error(message);
  err.status = status;
  return err;
}
module.exports = { errorHandler, createError };
JAVASCRIPT
// controllers/bookController.js
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");
function getAllBooks(req, res) {
  res.json({ success: true, data: Book.findAll() });
}
function getBookById(req, res, next) {
  const book = Book.findById(Number(req.params.id));
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}
function createBook(req, res) {
  const book = Book.create(req.body);
  res.status(201).json({ success: true, data: book });
}
function updateBook(req, res, next) {
  const book = Book.update(Number(req.params.id), req.body);
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}
function deleteBook(req, res, next) {
  const removed = Book.remove(Number(req.params.id));
  if (!removed) return next(createError(404, "Book not found"));
  res.json({ success: true, data: { message: "Book deleted" } });
}
module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };
JAVASCRIPT
// routes/books.js
const express = require("express");
const router = express.Router();
const ctrl = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");
router.get("/", ctrl.getAllBooks);
router.get("/:id", ctrl.getBookById);
router.post("/", validateBook, ctrl.createBook);
router.put("/:id", validateBook, ctrl.updateBook);
router.delete("/:id", ctrl.deleteBook);
module.exports = router;
JAVASCRIPT
// app.js
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/api/books", booksRoute);
app.use(errorHandler);
app.listen(3000, () => console.log("Server running on port 3000"));
BASH
# Start and Test
node app.js
curl -X POST -H "Content-Type: application/json" \
  -d '{"title":"Clean Code","author":"Robert","year":2008}' \
  localhost:3000/api/books
curl localhost:3000/api/books/1
curl -X DELETE localhost:3000/api/books/1

❓ よくある質問

Q なぜコントローラーとルートは分離されているのですか?
A 関心の分離のためです。ルートはパスのマッピングを定義し、コントローラーはビジネスロジックに焦点を当てています。これらを分離することで、それぞれを独立して変更・テストできるようになります。
Q API をテストするにはどうすればよいですか?
A コマンドラインでは curl を、グラフィカルインターフェースでは Postman または VS Code 用の Thunder Client プラグインを、自動テストでは supertest ライブラリを使用してください。
Q データ検証はどのレイヤーで行うべきですか?
A リクエストがコントローラーに到達する前に無効なデータを遮断し、コントローラーのロジックをクリーンな状態に保つため、ミドルウェア層で行うべきです。
Q 見つからないリソースはどのように扱うべきですか?
A 標準化されたエラー形式 { success: false, error: { code: 404, message: "Book not found" } } とともに、404 ステータスコードを返します。
Q 自分のプロジェクトでTypeScriptを使うべきですか?
A 小規模な練習用プロジェクトであればJavaScriptで十分ですが、大規模な本番環境向けのプロジェクトでは、型安全性やIDEのサポートが充実していることから、TypeScriptの使用をお勧めします。
Q 再起動後、メモリに保存されたデータは失われますか?
A はい。配列はプロセスメモリに保存されるため、サービスが再起動するとデータは消去されます。本番環境では、データベース(MongoDB、PostgreSQLなど)を使用することをお勧めします。
Q PUTとPATCHの違いは何ですか?
A PUTではリソース全体を指定して完全に置き換える必要がありますが、PATCHでは変更が必要なフィールドのみを送信して部分的な更新を行います。このレッスンでは、簡便さを考慮してPUTを使用します。

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%