Express & Fastify
ルール
- ルートはドメインごとに整理する:
routes/users.ts、routes/orders.ts — リソースごとに1つのルーター
- すべての入力を Zod でバリデーションする:処理の前に
req.body、req.params、req.query をパースする
- エラーハンドラーを一元化する:Express では catch-all ミドルウェア、Fastify では
setErrorHandler
- 一貫したエラー形式を返す:
{ error: { code: string, message: string, details?: unknown } }
- TypeScript:ジェネリクスでリクエスト/レスポンスを型付けする —
Request<Params, ResBody, ReqBody, Query>
- ミドルウェアの順序:CORS、ボディパーサー、認証、レート制限、ルート、エラーハンドラー
- async ルートハンドラーを使用する — エラーキャッチでラップするか、
express-async-errors を使用する
- Fastify:プラグインシステムを使用してルートとデコレータをカプセル化する —
fastify.register()
- Fastify スキーマ:JSON Schema を定義するか、
@fastify/type-provider-zod を使用してバリデーションとシリアライゼーションを行う
- ヘルスチェック:
GET /health — DB/Redis の接続状態を含めて 200 を返す
- グレースフルシャットダウン:SIGTERM をリッスンし、新しい接続を停止し、処理中のリクエストを完了させる
パターン
import { Router } from "express";
import { z } from "zod";
const router = Router();
const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(1) });
router.post("/users", async (req, res, next) => {
try {
const body = CreateUserSchema.parse(req.body);
const user = await createUser(body);
res.status(201).json(user);
} catch (err) { next(err); }
});
import Fastify from "fastify";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "fastify-type-provider-zod";
const app = Fastify().withTypeProvider<ZodTypeProvider>();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
app.post("/users", { schema: { body: CreateUserSchema } }, async (req) => {
return createUser(req.body);
});
避けるべきこと
- ルートハンドラーにビジネスロジックを置かない — サービス/ドメインレイヤーに抽出する
- Express での async エラーハンドリングを忘れない — ハンドルされていない拒否はプロセスをクラッシュさせる
- すべてのルートを1つのファイルにインポートしない — モジュール化のために
router.use() や Fastify プラグインを使用する
- 「フロントエンドでバリデーションしているから」とサーバーサイドのバリデーションを省略しない — 常にサーバーサイドでバリデーションする