Express + ベストプラクティス
この章では、ExpressとTypeScriptを組み合わせた型安全なバックエンド開発について学びます。
この章で学ぶこと
- Express + TypeScriptのセットアップ
- 型安全なリクエスト/レスポンス
- エラーハンドリング
- バリデーション(Zod)
- 型設計のベストプラクティス
TypeScriptをExpressで使うことで、APIの型安全性が向上し、リクエスト/レスポンスの構造を明確に定義できます。
Express + TypeScriptのセットアップ
本書では Express 5(2024 年 9 月 GA) を前提とします。Express 5 では async 関数のエラーが自動的に next に伝播される改善、より厳密な path-to-regexp 構文、require() のドロップ(ESM 推奨)など、いくつかの重要な改善が入っています。
# プロジェクトディレクトリを作成
mkdir express-api
cd express-api
npm init -y
# 依存関係のインストール(Express 5 を明示)
npm install express@^5 cors helmet
npm install --save-dev typescript @types/node @types/express @types/cors tsx
開発時の実行には、ESM対応で高速な tsx を使用します。旧来の ts-node-dev は CommonJS 主体で、ESM プロジェクトで不安定になりがちです。Node.js 22+ であれば node --watch --experimental-strip-types src/index.ts でも代替できます。
package.json("type": "module" を明示):
{
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
tsconfig.json(ESM + NodeNext 推奨):
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
CommonJS プロジェクトの場合
既存の CommonJS プロジェクトを維持したい場合は、"module": "commonjs" / "moduleResolution": "node" とし、package.json の "type": "module" を外します。ただし、2026年の新規プロジェクトでは Express 5 と合わせて ESM("type": "module" + "module": "NodeNext") を選ぶのが推奨です。
Expressアプリケーションの基本構造
// src/index.ts
// Expressと関連する型をインポート
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors'; // CORSミドルウェア
import helmet from 'helmet'; // セキュリティヘッダー設定
import { userRouter } from './routes/userRoutes';
import { errorHandler } from './middleware/errorHandler';
// Expressアプリケーションを作成
const app: Application = express();
// ポート番号(環境変数または3000)
const PORT = process.env.PORT || 3000;
// ===== ミドルウェアの設定 =====
app.use(helmet()); // セキュリティヘッダーを設定
app.use(cors()); // CORS(クロスオリジン)を許可
app.use(express.json()); // JSONボディをパース
// ===== ルートの設定 =====
// ルートパス
app.get('/', (req: Request, res: Response) => {
res.json({ message: 'Welcome to the API' });
});
// ユーザー関連のルート(/api/users/*)
app.use('/api/users', userRouter);
// ===== エラーハンドリング(最後に配置) =====
app.use(errorHandler);
// サーバー起動
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
型安全なリクエスト/レスポンス
まず、使用する型を定義します。
// src/types/index.ts
// ユーザーの型
export interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
// ユーザー作成時のDTO(Data Transfer Object)
// 作成時にはidやcreatedAtは不要
export interface CreateUserDto {
name: string;
email: string;
}
// ユーザー更新時のDTO
// すべてのフィールドがオプショナル
export interface UpdateUserDto {
name?: string;
email?: string;
}
// APIレスポンスの型(ジェネリック)
// 成功時はdata、失敗時はerrorを含む
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
次に、型安全なルートを作成します。
// src/routes/userRoutes.ts
import { Router, Request, Response, NextFunction } from 'express';
import { User, CreateUserDto, UpdateUserDto, ApiResponse } from '../types';
const router = Router();
// インメモリデータストア(実際はDBを使用)
let users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com', createdAt: new Date() },
{ id: 2, name: 'Bob', email: 'bob@example.com', createdAt: new Date() },
];
let nextId = 3;
// パラメータの型定義
interface UserParams {
id: string; // URLパラメータは常にstring
}
// ===== GET /api/users - 全ユーザー取得 =====
// Response<ApiResponse<User[]>>でレスポンスの型を指定
router.get('/', (req: Request, res: Response<ApiResponse<User[]>>) => {
res.json({
success: true,
data: users,
});
});
// ===== GET /api/users/:id - 特定ユーザー取得 =====
// Request<UserParams>でパラメータの型を指定
router.get('/:id', (
req: Request<UserParams>,
res: Response<ApiResponse<User>>
) => {
// パラメータを数値に変換
const id = parseInt(req.params.id, 10);
// ユーザーを検索
const user = users.find(u => u.id === id);
if (!user) {
// 404エラー
return res.status(404).json({
success: false,
error: 'User not found',
});
}
res.json({
success: true,
data: user,
});
});
// ===== POST /api/users - ユーザー作成 =====
// Request<Params, ResBody, ReqBody>の順で型を指定
router.post('/', (
req: Request<{}, ApiResponse<User>, CreateUserDto>,
res: Response<ApiResponse<User>>
) => {
const { name, email } = req.body;
// バリデーション
if (!name || !email) {
return res.status(400).json({
success: false,
error: 'Name and email are required',
});
}
// 新しいユーザーを作成
const newUser: User = {
id: nextId++,
name,
email,
createdAt: new Date(),
};
users.push(newUser);
// 201 Created
res.status(201).json({
success: true,
data: newUser,
message: 'User created successfully',
});
});
// ===== PUT /api/users/:id - ユーザー更新 =====
router.put('/:id', (
req: Request<UserParams, ApiResponse<User>, UpdateUserDto>,
res: Response<ApiResponse<User>>
) => {
const id = parseInt(req.params.id, 10);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({
success: false,
error: 'User not found',
});
}
const { name, email } = req.body;
// 存在するフィールドのみ更新
if (name) users[userIndex].name = name;
if (email) users[userIndex].email = email;
res.json({
success: true,
data: users[userIndex],
message: 'User updated successfully',
});
});
// ===== DELETE /api/users/:id - ユーザー削除 =====
router.delete('/:id', (
req: Request<UserParams>,
res: Response<ApiResponse<null>>
) => {
const id = parseInt(req.params.id, 10);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({
success: false,
error: 'User not found',
});
}
// 配列から削除
users.splice(userIndex, 1);
res.json({
success: true,
message: 'User deleted successfully',
});
});
export { router as userRouter };
エラーハンドリング
カスタムエラークラスとエラーハンドリングミドルウェアを作成します。
Express 5 では async エラーが自動で next に渡される
Express 4 では async 関数内で throw された例外は Express に捕捉されず、手動で try/catch + next(err) が必要でした。Express 5(2024 GA)からは async 関数が返した rejected Promise が自動的にエラーミドルウェアへ伝播します。そのため、以下のような冗長な try/catch が不要になりました。
// Express 4 で必要だった書き方
app.get('/users/:id', async (req, res, next) => {
try {
const user = await fetchUser(req.params.id)
res.json(user)
} catch (err) {
next(err) // 必須
}
})
// Express 5 では素直に書ける
app.get('/users/:id', async (req, res) => {
const user = await fetchUser(req.params.id)
res.json(user) // 例外は自動で errorHandler に流れる
})
それでもエラーメッセージを整形したいなどの理由で try/catch を使うのは OK です。意図的に next(err) する場合と、自動伝播させる場合を使い分けてください。
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { ApiResponse } from '../types';
// カスタムエラークラス
export class AppError extends Error {
statusCode: number; // HTTPステータスコード
isOperational: boolean; // 運用上のエラーかどうか
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // アプリケーションが想定するエラー
// スタックトレースを正しく取得
Error.captureStackTrace(this, this.constructor);
}
}
// エラーハンドリングミドルウェア
// 4つの引数を持つ関数はExpressでエラーハンドラとして認識される
export function errorHandler(
err: Error | AppError,
req: Request,
res: Response<ApiResponse<null>>,
next: NextFunction
): void {
console.error('Error:', err);
// AppErrorの場合は適切なステータスコードを返す
if (err instanceof AppError) {
res.status(err.statusCode).json({
success: false,
error: err.message,
});
return;
}
// 予期しないエラー(500 Internal Server Error)
res.status(500).json({
success: false,
error: 'Internal server error',
});
}
// 非同期エラーをキャッチするラッパー
// async関数のエラーを自動的にnext()に渡す
export function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
return (req: Request, res: Response, next: NextFunction) => {
// Promiseのエラーをキャッチしてnextに渡す
Promise.resolve(fn(req, res, next)).catch(next);
};
}
使用例:
// asyncHandlerとAppErrorを使用
import { asyncHandler, AppError } from '../middleware/errorHandler';
router.get('/:id', asyncHandler(async (req, res) => {
const id = parseInt(req.params.id, 10);
const user = await userService.findById(id);
if (!user) {
// AppErrorをthrowすると、errorHandlerがキャッチして処理
throw new AppError('User not found', 404);
}
res.json({ success: true, data: user });
}));
バリデーション(Zod)
Zodを使用した型安全なバリデーションを実装します。
npm install zod
// src/validation/userValidation.ts
import { z } from 'zod';
// スキーマ定義
// z.object()でオブジェクトスキーマを作成
export const createUserSchema = z.object({
name: z.string()
.min(2, 'Name must be at least 2 characters') // 最小2文字
.max(50, 'Name must be at most 50 characters'), // 最大50文字
email: z.email('Invalid email format'), // メール形式をチェック (v4 はトップレベル API)
});
export const updateUserSchema = z.object({
name: z.string()
.min(2)
.max(50)
.optional(), // オプショナル
email: z.email()
.optional(),
});
// スキーマから型を生成
// z.infer<typeof スキーマ>でスキーマから型を抽出
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
バリデーションミドルウェア:
// src/middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
import { ApiResponse } from '../types';
// ジェネリックなバリデーションミドルウェア
export function validate(schema: ZodSchema) {
return (req: Request, res: Response<ApiResponse<null>>, next: NextFunction) => {
// safeParse: エラーをthrowせず、結果オブジェクトを返す
const result = schema.safeParse(req.body);
if (!result.success) {
// バリデーションエラー
const errors = result.error.issues.map(e => ({
field: e.path.join('.'), // エラーのフィールドパス
message: e.message, // エラーメッセージ
}));
return res.status(400).json({
success: false,
error: 'Validation failed',
message: JSON.stringify(errors),
});
}
// バリデーション済みのデータで置き換え
req.body = result.data;
next();
};
}
ルートで使用:
// src/routes/userRoutes.ts
import { validate } from '../middleware/validate';
import { createUserSchema } from '../validation/userValidation';
// validateミドルウェアをルートに追加
router.post('/', validate(createUserSchema), (req, res) => {
// req.bodyはCreateUserInput型として安全に使用可能
const { name, email } = req.body;
// ...
});
型設計のベストプラクティス
1. DTOパターン
// リクエスト/レスポンスごとに適切な型を定義
// エンティティ(データベースの形)
interface User {
id: number;
name: string;
email: string;
passwordHash: string; // 内部でのみ使用
createdAt: Date;
updatedAt: Date;
}
// 作成時のDTO(必要なフィールドのみ)
interface CreateUserDto {
name: string;
email: string;
password: string; // パスワード(ハッシュ化前)
}
// レスポンス用DTO(パスワードを除外)
interface UserResponseDto {
id: number;
name: string;
email: string;
createdAt: Date;
}
// エンティティをレスポンス用に変換
function toUserResponse(user: User): UserResponseDto {
return {
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt,
};
}
2. 統一されたAPIレスポンス
// すべてのAPIで統一された形式を使用
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
meta?: {
total?: number;
page?: number;
limit?: number;
};
}
// 成功レスポンスを作成するヘルパー
function successResponse<T>(data: T, message?: string): ApiResponse<T> {
return {
success: true,
data,
message,
};
}
// エラーレスポンスを作成するヘルパー
function errorResponse(error: string, message?: string): ApiResponse<null> {
return {
success: false,
error,
message,
};
}
3. 型ガード
// 型ガード関数でランタイムでも型を保証
// unknownからUserかどうかをチェック
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// 使用例
const data: unknown = await fetchData();
if (isUser(data)) {
// この中ではdataはUser型として扱える
console.log(data.name);
}
4. 環境変数の型付け
// src/config/env.ts
import { z } from 'zod';
// 環境変数のスキーマ
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number), // 文字列を数値に変換
DATABASE_URL: z.url(),
JWT_SECRET: z.string().min(32),
});
// 環境変数をパースして型付け
export const env = envSchema.parse(process.env);
// env.PORT は number 型
// env.DATABASE_URL は string 型
5. サービス層の型付け
// src/services/userService.ts
// サービスの戻り値型
interface ServiceResult<T> {
success: boolean;
data?: T;
error?: string;
}
class UserService {
// ユーザー検索
async findById(id: number): Promise<ServiceResult<User>> {
try {
const user = await db.users.findUnique({ where: { id } });
if (!user) {
return { success: false, error: 'User not found' };
}
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Database error' };
}
}
// ユーザー作成
async create(dto: CreateUserDto): Promise<ServiceResult<User>> {
try {
const user = await db.users.create({ data: dto });
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Failed to create user' };
}
}
}
試してみよう: 型安全なAPIエンドポイントを作ろう ★★★
以下の要件を満たすTodo APIを作成してください。
要件:
GET /api/todos- 全Todoを取得POST /api/todos- Todoを作成(title必須)PATCH /api/todos/:id/toggle- 完了状態を切り替え- Zodでバリデーション
- エラーハンドリング
ヒント
- Todo型を定義(id, title, completed, createdAt)
- CreateTodoSchemaをZodで作成
- validateミドルウェアを使用
- asyncHandlerで非同期エラーをキャッチ
- AppErrorで適切なエラーを返す
回答と解説
// src/types/todo.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
export interface CreateTodoDto {
title: string;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// src/validation/todoValidation.ts
import { z } from 'zod';
// タイトル必須、1〜100文字
export const createTodoSchema = z.object({
title: z.string()
.min(1, 'Title is required')
.max(100, 'Title must be at most 100 characters'),
});
export type CreateTodoInput = z.infer<typeof createTodoSchema>;
// src/routes/todoRoutes.ts
import { Router, Request, Response } from 'express';
import { Todo, CreateTodoDto, ApiResponse } from '../types/todo';
import { asyncHandler, AppError } from '../middleware/errorHandler';
import { validate } from '../middleware/validate';
import { createTodoSchema } from '../validation/todoValidation';
const router = Router();
// インメモリデータストア
let todos: Todo[] = [];
let nextId = 1;
// パラメータの型
interface TodoParams {
id: string;
}
// GET /api/todos - 全Todo取得
router.get('/', (req: Request, res: Response<ApiResponse<Todo[]>>) => {
res.json({
success: true,
data: todos,
});
});
// POST /api/todos - Todo作成
router.post('/',
// バリデーションミドルウェア
validate(createTodoSchema),
(req: Request<{}, ApiResponse<Todo>, CreateTodoDto>, res: Response<ApiResponse<Todo>>) => {
const { title } = req.body;
const newTodo: Todo = {
id: nextId++,
title,
completed: false,
createdAt: new Date(),
};
todos.push(newTodo);
res.status(201).json({
success: true,
data: newTodo,
message: 'Todo created successfully',
});
}
);
// PATCH /api/todos/:id/toggle - 完了状態切り替え
router.patch('/:id/toggle',
asyncHandler(async (req: Request<TodoParams>, res: Response<ApiResponse<Todo>>) => {
const id = parseInt(req.params.id, 10);
// 無効なIDチェック
if (isNaN(id)) {
throw new AppError('Invalid todo ID', 400);
}
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
throw new AppError('Todo not found', 404);
}
// 完了状態を反転
todos[todoIndex].completed = !todos[todoIndex].completed;
res.json({
success: true,
data: todos[todoIndex],
message: `Todo marked as ${todos[todoIndex].completed ? 'completed' : 'incomplete'}`,
});
})
);
// DELETE /api/todos/:id - Todo削除
router.delete('/:id',
asyncHandler(async (req: Request<TodoParams>, res: Response<ApiResponse<null>>) => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
throw new AppError('Invalid todo ID', 400);
}
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
throw new AppError('Todo not found', 404);
}
todos.splice(todoIndex, 1);
res.json({
success: true,
message: 'Todo deleted successfully',
});
})
);
export { router as todoRouter };
// src/index.ts(ルートを追加)
import { todoRouter } from './routes/todoRoutes';
// ...
app.use('/api/todos', todoRouter);
// ...
解説:
- 型定義:
TodoインターフェースでTodoの構造を定義し、CreateTodoDtoで作成時の入力を定義しています - Zodバリデーション:
createTodoSchemaでタイトルの必須チェックと長さ制限を設定しています - validateミドルウェア: ルートに追加することで、ハンドラに到達する前にバリデーションが実行されます
- asyncHandler: 非同期処理のエラーを自動的にキャッチし、エラーハンドラに渡します
- AppError: 適切なHTTPステータスコードとメッセージでエラーを返します
- 型安全なレスポンス:
Response<ApiResponse<T>>で統一された形式を保証しています
テスト方法:
# サーバー起動
npm run dev
# Todo作成
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title": "Learn TypeScript"}'
# 全Todo取得
curl http://localhost:3000/api/todos
# 完了状態切り替え
curl -X PATCH http://localhost:3000/api/todos/1/toggle
# Todo削除
curl -X DELETE http://localhost:3000/api/todos/1
Express 以外の選択肢(2026年)
Express は依然として最も普及している Node.js フレームワークですが、2026 年時点では用途に応じて以下の代替を検討できます。
| フレームワーク | 特徴 | 向いているケース |
|---|---|---|
| Express 5 | 安定性・実績・豊富なエコシステム | 既存の Express 資産を活かす、学習目的 |
| Fastify | Express 互換の API で高速、スキーマ駆動 | パフォーマンス重視の REST API |
| Hono | 軽量・Web 標準 API ベース、エッジ環境で動く | Cloudflare Workers / Deno Deploy などのエッジデプロイ |
| Elysia | Bun に最適化、型推論が効きやすい | Bun を採用している新規プロジェクト |
| NestJS | Decorator ベースの大規模向け FW | エンタープライズ向け、DDD / Clean Architecture |
SPA のバックエンドとしては Hono が特に人気を伸ばしています。Web 標準(Fetch API / Web Streams)をベースにしており、Cloudflare Workers・Deno Deploy・Node.js・Bun など様々なランタイムで同じコードが動きます。本書では Express を題材にしますが、新規プロジェクトでは用途に応じて使い分けを検討してください。
さらに学ぶべきトピック
本章で基本を学んだ後、実際のプロダクション開発では以下のトピックが重要です。
ORM(Prisma)
データベース操作には型安全なORM「Prisma」が人気です。
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
// 自動生成される型を使用
import { PrismaClient, User } from '@prisma/client';
const prisma = new PrismaClient();
// 完全に型安全なクエリ
async function getUser(id: number): Promise<User | null> {
return prisma.user.findUnique({ where: { id } });
}
// リレーションも型安全
async function getUserWithPosts(id: number) {
return prisma.user.findUnique({
where: { id },
include: { posts: true }, // 型推論される
});
}
JWT認証
認証にはJWT(JSON Web Token)がよく使われます。
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
// JWTペイロードの型
interface JwtPayload {
userId: number;
email: string;
}
// 認証済みリクエストの型
interface AuthRequest extends Request {
user?: JwtPayload;
}
// トークン生成
function generateToken(payload: JwtPayload): string {
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '24h' });
}
// 認証ミドルウェア
function authMiddleware(req: AuthRequest, res: Response, next: NextFunction): void {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
res.status(401).json({ error: 'No token provided' });
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
セッション管理
セッションベースの認証にはexpress-sessionを使います。
import session from 'express-session';
// セッションの型を拡張
declare module 'express-session' {
interface SessionData {
userId: number;
isLoggedIn: boolean;
}
}
app.use(session({
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
}));
// 使用例
app.post('/login', (req, res) => {
req.session.userId = 1;
req.session.isLoggedIn = true;
res.json({ message: 'Logged in' });
});
まとめ
この章で学んだこと:
- Express + TypeScriptのセットアップ: プロジェクト構成と設定ファイル
- 型安全なリクエスト/レスポンス:
Request<Params, ResBody, ReqBody>の活用 - エラーハンドリング: AppErrorクラスとerrorHandlerミドルウェア
- asyncHandler: 非同期エラーの自動キャッチ
- Zodバリデーション: スキーマ定義と型生成
- validateミドルウェア: ルートへのバリデーション適用
- 型設計のベストプラクティス: DTO、統一レスポンス、型ガード
次の章では、総合演習プロジェクトとしてCLIタスク管理ツールを作成します。
初学者がつまずきやすいポイント
よくある間違い
❌ Requestの型パラメータの順序を間違える
// Request<Params, ResBody, ReqBody, Query>の順序
// ❌ 順序を間違える
router.post('/', (req: Request<CreateUserDto>) => { // 第1引数はParams
// ...
});
// ✅ 正しい順序
router.post('/', (
req: Request<{}, ApiResponse<User>, CreateUserDto>, // 第3引数がReqBody
res: Response<ApiResponse<User>>
) => {
const { name } = req.body; // CreateUserDto型
});
原因: Requestの型パラメータは<Params, ResBody, ReqBody, Query>の順序です。
解決策: ReqBodyは第3引数、空のパラメータには{}を指定しましょう。
❌ 非同期エラーがキャッチされない
// ❌ asyncルートのエラーがExpressに伝わらない
router.get('/', async (req, res) => {
const data = await someAsyncOperation(); // エラーが発生すると...
res.json(data); // クラッシュまたはハング
});
// ✅ asyncHandlerでラップ
router.get('/', asyncHandler(async (req, res) => {
const data = await someAsyncOperation();
res.json(data);
}));
// asyncHandlerの実装
function asyncHandler(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
原因: Expressは非同期関数のエラーを自動的にキャッチしません。
解決策: asyncHandlerでラップするか、express-async-errorsパッケージを使いましょう。
❌ URLパラメータが常にstringであることを忘れる
// ❌ パラメータを数値として直接使用
router.get('/:id', (req, res) => {
const id = req.params.id; // string型
const user = users.find(u => u.id === id); // 比較が常にfalseになる可能性
});
// ✅ 明示的に変換
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}
const user = users.find(u => u.id === id);
});
原因: URLパラメータは常にstring型であり、数値への変換が必要です。
解決策: parseIntで変換し、isNaNでバリデーションしましょう。
❌ Zodのsafeparseを使わずにthrowする
// ❌ parseはエラーをthrowする
try {
const data = schema.parse(req.body);
} catch (e) {
// ZodErrorの処理が複雑
}
// ✅ safeParseは結果オブジェクトを返す
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.issues,
});
}
const data = result.data; // 型安全なデータ
原因: parseはエラー時に例外をスローし、適切なエラーレスポンスを返しにくくなります。
解決策: safeParseを使い、successフラグで分岐しましょう。
❌ エラーハンドリングミドルウェアの引数が3つ
// ❌ 引数が3つだとエラーハンドラとして認識されない
app.use((req, res, next) => {
// 通常のミドルウェアとして扱われる
});
// ✅ 4つの引数でエラーハンドラとして認識
app.use((err, req, res, next) => { // 4つの引数が必須
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
原因: Expressは引数が4つの関数をエラーハンドリングミドルウェアとして認識します。
解決策: 必ず4つの引数(err, req, res, next)を持たせましょう。