Express.jsは、Node.jsで最も広く使われているWebアプリケーションフレームワークです。シンプルなAPIから大規模なWebサービスまで、柔軟に対応できる軽量さが特徴です。
本記事では、Express.jsを使ったREST APIの構築方法を、プロジェクトセットアップからルーティング、ミドルウェア、エラーハンドリング、認証まで段階的に解説します。
Express.jsとREST APIの基本
REST(Representational State Transfer)は、HTTPメソッド(GET/POST/PUT/DELETE)を使ってリソースを操作するAPIの設計思想です。Express.jsはこのRESTfulなAPIを簡潔に記述するための機能を提供します。
プロジェクトセットアップ
# プロジェクト初期化
mkdir express-api && cd express-api
pnpm init
# 必要なパッケージのインストール
pnpm add express cors
pnpm add -D typescript @types/express @types/cors @types/node tsx
npx tsc --init
// package.json
{
"name": "express-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
基本的なサーバー構築
// src/index.ts
import express from "express";
import cors from "cors";
const app = express();
// 基本ミドルウェア
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ヘルスチェック
app.get("/api/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
export default app;
ルーティングの設計と実装
RESTful APIでは、URLとHTTPメソッドの組み合わせでリソースの操作を表現します。
基本的なCRUDルーティング
// src/types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: string;
updatedAt: string;
}
export type UserCreateInput = Omit<User, "id" | "createdAt" | "updatedAt">;
export type UserUpdateInput = Partial<UserCreateInput>;
// src/routes/users.ts
import { Router, Request, Response } from "express";
import type { User, UserCreateInput, UserUpdateInput } from "../types/user.js";
const router = Router();
// インメモリデータストア(実務ではDBを使用)
let users: User[] = [
{
id: 1,
name: "田中太郎",
email: "tanaka@example.com",
role: "admin",
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
},
];
let nextId = 2;
// GET /api/users - ユーザー一覧取得
router.get("/", (req: Request, res: Response) => {
const { role, search } = req.query;
let result = [...users];
if (typeof role === "string") {
result = result.filter((u) => u.role === role);
}
if (typeof search === "string") {
const keyword = search.toLowerCase();
result = result.filter(
(u) =>
u.name.toLowerCase().includes(keyword) ||
u.email.toLowerCase().includes(keyword)
);
}
res.json({
success: true,
data: result,
total: result.length,
});
});
// GET /api/users/:id - ユーザー詳細取得
router.get("/:id", (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
const user = users.find((u) => u.id === id);
if (!user) {
return res.status(404).json({
success: false,
error: { code: "NOT_FOUND", message: "ユーザーが見つかりません" },
});
}
res.json({ success: true, data: user });
});
// POST /api/users - ユーザー作成
router.post("/", (req: Request, res: Response) => {
const input: UserCreateInput = req.body;
const now = new Date().toISOString();
const newUser: User = {
...input,
id: nextId++,
createdAt: now,
updatedAt: now,
};
users.push(newUser);
res.status(201).json({ success: true, data: newUser });
});
// PUT /api/users/:id - ユーザー更新
router.put("/:id", (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
const index = users.findIndex((u) => u.id === id);
if (index === -1) {
return res.status(404).json({
success: false,
error: { code: "NOT_FOUND", message: "ユーザーが見つかりません" },
});
}
const update: UserUpdateInput = req.body;
users[index] = {
...users[index],
...update,
updatedAt: new Date().toISOString(),
};
res.json({ success: true, data: users[index] });
});
// DELETE /api/users/:id - ユーザー削除
router.delete("/:id", (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
const index = users.findIndex((u) => u.id === id);
if (index === -1) {
return res.status(404).json({
success: false,
error: { code: "NOT_FOUND", message: "ユーザーが見つかりません" },
});
}
users.splice(index, 1);
res.status(204).send();
});
export default router;
ルーターの統合
// src/index.ts にルーターを追加
import userRouter from "./routes/users.js";
app.use("/api/users", userRouter);
ミドルウェアの仕組みと実装
ミドルウェアは、リクエストとレスポンスの間に挟まる処理です。Express.jsの核心的な概念であり、ロギング、認証、バリデーション、エラーハンドリングなどを横断的に適用できます。
リクエストログミドルウェア
// src/middleware/logger.ts
import { Request, Response, NextFunction } from "express";
export function requestLogger(req: Request, res: Response, next: NextFunction): void {
const start = Date.now();
// レスポンス完了時にログ出力
res.on("finish", () => {
const duration = Date.now() - start;
const log = {
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: `${duration}ms`,
timestamp: new Date().toISOString(),
};
console.log(JSON.stringify(log));
});
next();
}
バリデーションミドルウェア(Zod連携)
// src/middleware/validate.ts
import { Request, Response, NextFunction } from "express";
import { ZodSchema, ZodError } from "zod";
interface ValidateTarget {
body?: ZodSchema;
query?: ZodSchema;
params?: ZodSchema;
}
export function validate(schemas: ValidateTarget) {
return (req: Request, res: Response, next: NextFunction): void => {
const errors: Array<{ field: string; message: string }> = [];
for (const [target, schema] of Object.entries(schemas)) {
if (!schema) continue;
const data = req[target as keyof typeof schemas];
const result = schema.safeParse(data);
if (!result.success) {
for (const issue of result.error.issues) {
errors.push({
field: `${target}.${issue.path.join(".")}`,
message: issue.message,
});
}
} else {
// パース結果で上書き(transformやdefaultが反映される)
(req as any)[target] = result.data;
}
}
if (errors.length > 0) {
res.status(400).json({
success: false,
error: { code: "VALIDATION_ERROR", message: "入力値が不正です", details: errors },
});
return;
}
next();
};
}
// ルートでの使用例
import { z } from "zod";
import { validate } from "../middleware/validate.js";
const createUserSchema = z.object({
name: z.string().min(1, "名前は必須です").max(100),
email: z.string().email("メールアドレスの形式が不正です"),
role: z.enum(["admin", "editor", "viewer"]).default("viewer"),
});
router.post(
"/",
validate({ body: createUserSchema }),
(req: Request, res: Response) => {
// req.body はバリデーション済み
const input = req.body;
// ...
}
);
レート制限ミドルウェア
// src/middleware/rateLimit.ts
import { Request, Response, NextFunction } from "express";
interface RateLimitStore {
[key: string]: { count: number; resetAt: number };
}
export function rateLimit(options: { windowMs: number; max: number }) {
const store: RateLimitStore = {};
return (req: Request, res: Response, next: NextFunction): void => {
const key = req.ip || "unknown";
const now = Date.now();
if (!store[key] || store[key].resetAt < now) {
store[key] = { count: 0, resetAt: now + options.windowMs };
}
store[key].count++;
const remaining = Math.max(0, options.max - store[key].count);
res.set("X-RateLimit-Limit", String(options.max));
res.set("X-RateLimit-Remaining", String(remaining));
res.set("X-RateLimit-Reset", String(store[key].resetAt));
if (store[key].count > options.max) {
res.status(429).json({
success: false,
error: { code: "RATE_LIMIT", message: "リクエスト数が上限を超えました" },
});
return;
}
next();
};
}
// 適用例:1分あたり60リクエストまで
app.use("/api/", rateLimit({ windowMs: 60 * 1000, max: 60 }));
エラーハンドリング
Express.jsでは、4つの引数を持つミドルウェアがエラーハンドラーとして認識されます。
カスタムエラークラス
// src/errors/AppError.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code: string = "INTERNAL_ERROR"
) {
super(message);
this.name = "AppError";
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource}が見つかりません`, 404, "NOT_FOUND");
}
}
export class UnauthorizedError extends AppError {
constructor(message = "認証が必要です") {
super(message, 401, "UNAUTHORIZED");
}
}
export class ForbiddenError extends AppError {
constructor(message = "アクセス権限がありません") {
super(message, 403, "FORBIDDEN");
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(message, 409, "CONFLICT");
}
}
グローバルエラーハンドラー
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import { AppError } from "../errors/AppError.js";
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
): void {
// AppErrorの場合はステータスコードとメッセージを使用
if (err instanceof AppError) {
res.status(err.statusCode).json({
success: false,
error: {
code: err.code,
message: err.message,
},
});
return;
}
// 予期しないエラー
console.error("Unexpected error:", err);
res.status(500).json({
success: false,
error: {
code: "INTERNAL_ERROR",
message:
process.env.NODE_ENV === "production"
? "サーバー内部エラーが発生しました"
: err.message,
},
});
}
// 404ハンドラー
export function notFoundHandler(req: Request, res: Response): void {
res.status(404).json({
success: false,
error: {
code: "NOT_FOUND",
message: `${req.method} ${req.path} は存在しません`,
},
});
}
// src/index.ts にエラーハンドラーを登録(ルーターの後に配置)
import { errorHandler, notFoundHandler } from "./middleware/errorHandler.js";
app.use("/api/users", userRouter);
// 404ハンドラー(すべてのルートの後)
app.use(notFoundHandler);
// エラーハンドラー(最後に配置)
app.use(errorHandler);
非同期エラーのキャッチ
// asyncHandler ラッパー
import { Request, Response, NextFunction } from "express";
function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
return (req: Request, res: Response, next: NextFunction) => {
fn(req, res, next).catch(next);
};
}
// 使用例
router.get(
"/:id",
asyncHandler(async (req, res) => {
const id = parseInt(req.params.id, 10);
const user = await findUserById(id); // エラーが投げられたら自動でnextに渡る
if (!user) {
throw new NotFoundError("ユーザー");
}
res.json({ success: true, data: user });
})
);
認証・認可の実装
JWT認証ミドルウェア
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { UnauthorizedError, ForbiddenError } from "../errors/AppError.js";
interface JwtPayload {
userId: number;
role: string;
}
// Requestオブジェクトにuserを追加
declare global {
namespace Express {
interface Request {
user?: JwtPayload;
}
}
}
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
// 認証ミドルウェア
export function authenticate(req: Request, res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
throw new UnauthorizedError("Bearerトークンが必要です");
}
const token = authHeader.split(" ")[1];
try {
const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
req.user = payload;
next();
} catch (error) {
throw new UnauthorizedError("トークンが無効です");
}
}
// 認可ミドルウェア(ロールベース)
export function authorize(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user) {
throw new UnauthorizedError();
}
if (!roles.includes(req.user.role)) {
throw new ForbiddenError(`この操作には${roles.join("または")}権限が必要です`);
}
next();
};
}
// ログインエンドポイント
router.post("/login", asyncHandler(async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) {
throw new UnauthorizedError("メールアドレスまたはパスワードが正しくありません");
}
const token = jwt.sign(
{ userId: user.id, role: user.role },
JWT_SECRET,
{ expiresIn: "24h" }
);
res.json({ success: true, data: { token, user } });
}));
認証・認可の適用
// 認証が必要なルート
router.get("/me", authenticate, asyncHandler(async (req, res) => {
const user = await findUserById(req.user!.userId);
res.json({ success: true, data: user });
}));
// 管理者のみアクセス可能なルート
router.delete(
"/:id",
authenticate,
authorize("admin"),
asyncHandler(async (req, res) => {
await deleteUser(parseInt(req.params.id, 10));
res.status(204).send();
})
);
プロジェクト構成のベストプラクティス
推奨ディレクトリ構成
src/
├── index.ts # アプリケーションのエントリポイント
├── app.ts # Expressアプリケーション設定
├── routes/
│ ├── index.ts # ルーター統合
│ ├── users.ts # ユーザー関連ルート
│ └── auth.ts # 認証関連ルート
├── middleware/
│ ├── logger.ts
│ ├── auth.ts
│ ├── validate.ts
│ ├── rateLimit.ts
│ └── errorHandler.ts
├── services/
│ ├── userService.ts # ビジネスロジック
│ └── authService.ts
├── repositories/
│ └── userRepository.ts # データアクセス
├── errors/
│ └── AppError.ts
├── types/
│ └── user.ts
└── utils/
└── helpers.ts
サービス層の分離
// src/services/userService.ts
import type { User, UserCreateInput, UserUpdateInput } from "../types/user.js";
import { NotFoundError, ConflictError } from "../errors/AppError.js";
export class UserService {
async findAll(filter?: { role?: string; search?: string }): Promise<User[]> {
// データベースクエリ
return [];
}
async findById(id: number): Promise<User> {
const user = await db.user.findUnique({ where: { id } });
if (!user) throw new NotFoundError("ユーザー");
return user;
}
async create(input: UserCreateInput): Promise<User> {
const existing = await db.user.findUnique({ where: { email: input.email } });
if (existing) throw new ConflictError("このメールアドレスは既に使用されています");
return db.user.create({ data: input });
}
async update(id: number, input: UserUpdateInput): Promise<User> {
await this.findById(id); // 存在確認
return db.user.update({ where: { id }, data: input });
}
async delete(id: number): Promise<void> {
await this.findById(id); // 存在確認
await db.user.delete({ where: { id } });
}
}
まとめ
Express.jsを使ったREST API構築のポイントを整理します。
ルーティング:Routerを使ってリソースごとにファイルを分割し、RESTful な URL設計(GET/POST/PUT/DELETE)に沿ったエンドポイントを定義します。
ミドルウェア:ロギング、バリデーション(Zod連携)、レート制限、認証など、横断的な処理はミドルウェアとして実装し、再利用性を高めます。
エラーハンドリング:カスタムエラークラスを定義し、グローバルエラーハンドラーで一元的に処理します。asyncHandlerで非同期エラーも確実にキャッチします。
認証・認可:JWTを使った認証ミドルウェアとロールベースの認可ミドルウェアで、APIのセキュリティを確保します。
プロジェクト構成:ルート、ミドルウェア、サービス、リポジトリ、エラーを適切に分離し、責務の明確なコードを維持します。
Express.jsはシンプルですが拡張性が高く、ミドルウェアを組み合わせることで堅牢なAPIを構築できます。まずは基本的なCRUDエンドポイントから始め、段階的にバリデーションや認証を追加していきましょう。
関連記事
AIエージェント開発入門|自律型AIの仕組みと構築方法を解説【2026年版】
AI駆動コーディングワークフロー|Claude Code・Cursor・Copilotの実践的使い分け
プロンプトエンジニアリング上級編|Chain-of-Thought・Few-Shot・ReActの実践
APIレート制限の設計と実装|トークンバケット・スライディングウィンドウ解説
APIバージョニング戦略|URL・ヘッダー・クエリパラメータの使い分け
BIツール入門|Metabase・Redash・Looker Studioでデータ可視化する方法
チャットボット開発入門|LINE Bot・Slack Botの構築方法と活用事例
CI/CDパイプラインの基礎|継続的インテグレーション・デリバリーの全体像