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)でした!