Node.js + Express入門:RESTful APIの作り方

Node.jsとExpressを使ってRESTful APIを構築する方法を、基本から実践まで詳しく解説します。

Node.js + Express 入門:RESTful API の作り方

Node.js と Express を使用して、実用的な RESTful API を構築する方法を学びましょう。

環境セットアップ

プロジェクトの初期化

mkdir my-api
cd my-api
npm init -y

# 必要なパッケージのインストール
npm install express cors helmet morgan dotenv
npm install -D nodemon typescript @types/node @types/express

基本的なサーバー設定

// server.js
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
require("dotenv").config();

const app = express();
const PORT = process.env.PORT || 3000;

// ミドルウェア
app.use(helmet());
app.use(cors());
app.use(morgan("combined"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 基本的なルート
app.get("/", (req, res) => {
  res.json({ message: "APIサーバーが起動しています" });
});

app.listen(PORT, () => {
  console.log(`サーバーがポート${PORT}で起動しました`);
});

RESTful API 設計

データモデルの定義

// models/User.js
class User {
  constructor(id, name, email, createdAt = new Date()) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.createdAt = createdAt;
  }

  static validate(userData) {
    const errors = [];

    if (!userData.name || userData.name.trim().length < 2) {
      errors.push("名前は2文字以上である必要があります");
    }

    if (!userData.email || !this.isValidEmail(userData.email)) {
      errors.push("有効なメールアドレスを入力してください");
    }

    return errors;
  }

  static isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

module.exports = User;

ルーターの実装

// routes/users.js
const express = require("express");
const User = require("../models/User");

const router = express.Router();

// インメモリデータベース(実際のプロジェクトではDBを使用)
let users = [
  new User(1, "山田太郎", "yamada@example.com"),
  new User(2, "佐藤花子", "sato@example.com"),
];
let nextId = 3;

// GET /api/users - 全ユーザー取得
router.get("/", (req, res) => {
  try {
    const { page = 1, limit = 10, search } = req.query;
    let filteredUsers = users;

    // 検索機能
    if (search) {
      filteredUsers = users.filter(
        (user) => user.name.includes(search) || user.email.includes(search)
      );
    }

    // ページネーション
    const startIndex = (page - 1) * limit;
    const endIndex = page * limit;
    const paginatedUsers = filteredUsers.slice(startIndex, endIndex);

    res.json({
      users: paginatedUsers,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total: filteredUsers.length,
        totalPages: Math.ceil(filteredUsers.length / limit),
      },
    });
  } catch (error) {
    res.status(500).json({ error: "サーバーエラーが発生しました" });
  }
});

// GET /api/users/:id - 特定ユーザー取得
router.get("/:id", (req, res) => {
  try {
    const id = parseInt(req.params.id);
    const user = users.find((u) => u.id === id);

    if (!user) {
      return res.status(404).json({ error: "ユーザーが見つかりません" });
    }

    res.json(user);
  } catch (error) {
    res.status(500).json({ error: "サーバーエラーが発生しました" });
  }
});

// POST /api/users - ユーザー作成
router.post("/", (req, res) => {
  try {
    const { name, email } = req.body;

    // バリデーション
    const errors = User.validate({ name, email });
    if (errors.length > 0) {
      return res.status(400).json({ errors });
    }

    // 重複チェック
    const existingUser = users.find((u) => u.email === email);
    if (existingUser) {
      return res
        .status(400)
        .json({ error: "このメールアドレスは既に使用されています" });
    }

    const newUser = new User(nextId++, name, email);
    users.push(newUser);

    res.status(201).json(newUser);
  } catch (error) {
    res.status(500).json({ error: "サーバーエラーが発生しました" });
  }
});

// PUT /api/users/:id - ユーザー更新
router.put("/:id", (req, res) => {
  try {
    const id = parseInt(req.params.id);
    const userIndex = users.findIndex((u) => u.id === id);

    if (userIndex === -1) {
      return res.status(404).json({ error: "ユーザーが見つかりません" });
    }

    const { name, email } = req.body;

    // バリデーション
    const errors = User.validate({ name, email });
    if (errors.length > 0) {
      return res.status(400).json({ errors });
    }

    // 重複チェック(自分以外)
    const existingUser = users.find((u) => u.email === email && u.id !== id);
    if (existingUser) {
      return res
        .status(400)
        .json({ error: "このメールアドレスは既に使用されています" });
    }

    users[userIndex] = { ...users[userIndex], name, email };
    res.json(users[userIndex]);
  } catch (error) {
    res.status(500).json({ error: "サーバーエラーが発生しました" });
  }
});

// DELETE /api/users/:id - ユーザー削除
router.delete("/:id", (req, res) => {
  try {
    const id = parseInt(req.params.id);
    const userIndex = users.findIndex((u) => u.id === id);

    if (userIndex === -1) {
      return res.status(404).json({ error: "ユーザーが見つかりません" });
    }

    users.splice(userIndex, 1);
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: "サーバーエラーが発生しました" });
  }
});

module.exports = router;

ミドルウェア実装

認証ミドルウェア

// middleware/auth.js
const jwt = require("jsonwebtoken");

function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.status(401).json({ error: "アクセストークンが必要です" });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: "無効なトークンです" });
    }
    req.user = user;
    next();
  });
}

module.exports = { authenticateToken };

バリデーションミドルウェア

// middleware/validation.js
function validateUser(req, res, next) {
  const { name, email } = req.body;
  const errors = [];

  if (!name || name.trim().length < 2) {
    errors.push("名前は2文字以上である必要があります");
  }

  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    errors.push("有効なメールアドレスを入力してください");
  }

  if (errors.length > 0) {
    return res.status(400).json({ errors });
  }

  next();
}

function validatePagination(req, res, next) {
  const { page = 1, limit = 10 } = req.query;

  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);

  if (isNaN(pageNum) || pageNum < 1) {
    return res
      .status(400)
      .json({ error: "ページ番号は1以上の数値である必要があります" });
  }

  if (isNaN(limitNum) || limitNum < 1 || limitNum > 100) {
    return res
      .status(400)
      .json({ error: "limitは1-100の範囲で指定してください" });
  }

  req.pagination = { page: pageNum, limit: limitNum };
  next();
}

module.exports = { validateUser, validatePagination };

エラーハンドリング

// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  console.error(err.stack);

  // モンゴエラーの処理
  if (err.name === "ValidationError") {
    const errors = Object.values(err.errors).map((e) => e.message);
    return res.status(400).json({ errors });
  }

  // 重複キーエラー
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return res.status(400).json({
      error: `${field}は既に使用されています`,
    });
  }

  // JWTエラー
  if (err.name === "JsonWebTokenError") {
    return res.status(401).json({ error: "無効なトークンです" });
  }

  // デフォルトエラー
  res.status(500).json({
    error: "サーバーエラーが発生しました",
    ...(process.env.NODE_ENV === "development" && { details: err.message }),
  });
}

function notFound(req, res) {
  res.status(404).json({ error: "エンドポイントが見つかりません" });
}

module.exports = { errorHandler, notFound };

完全なサーバー設定

// server.js
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
const rateLimit = require("express-rate-limit");
require("dotenv").config();

const userRoutes = require("./routes/users");
const { errorHandler, notFound } = require("./middleware/errorHandler");

const app = express();
const PORT = process.env.PORT || 3000;

// レート制限
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100, // 最大100リクエスト
  message: "リクエストが多すぎます。しばらく時間をおいてからお試しください。",
});

// ミドルウェア
app.use(helmet());
app.use(cors());
app.use(morgan("combined"));
app.use(limiter);
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));

// ルート
app.get("/", (req, res) => {
  res.json({
    message: "APIサーバーが起動しています",
    version: "1.0.0",
    endpoints: {
      users: "/api/users",
    },
  });
});

app.use("/api/users", userRoutes);

// エラーハンドリング
app.use(notFound);
app.use(errorHandler);

app.listen(PORT, () => {
  console.log(`サーバーがポート${PORT}で起動しました`);
});

module.exports = app;

テスト

// tests/users.test.js
const request = require("supertest");
const app = require("../server");

describe("Users API", () => {
  describe("GET /api/users", () => {
    it("should return all users", async () => {
      const res = await request(app).get("/api/users").expect(200);

      expect(res.body.users).toBeInstanceOf(Array);
      expect(res.body.pagination).toBeDefined();
    });
  });

  describe("POST /api/users", () => {
    it("should create a new user", async () => {
      const userData = {
        name: "テストユーザー",
        email: "test@example.com",
      };

      const res = await request(app)
        .post("/api/users")
        .send(userData)
        .expect(201);

      expect(res.body.name).toBe(userData.name);
      expect(res.body.email).toBe(userData.email);
      expect(res.body.id).toBeDefined();
    });

    it("should validate required fields", async () => {
      const res = await request(app).post("/api/users").send({}).expect(400);

      expect(res.body.errors).toBeInstanceOf(Array);
      expect(res.body.errors.length).toBeGreaterThan(0);
    });
  });
});

まとめ

このガイドでは、Node.js と Express を使用した RESTful API の構築方法を詳しく説明しました。実際のプロジェクトでは、データベース(MongoDB、PostgreSQL など)の統合、認証システムの実装、デプロイメントの設定なども必要になります。

基本的な構造を理解して、段階的に機能を追加していくことで、スケーラブルな API を構築できます。

最後まで読んでいただきありがとうございました!てばさん(@basabasa8770)でした!

この記事をシェア