Documentation Index
Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
Use this file to discover all available pages before exploring further.
Devin にモノリスを見せる
おなじみのあのファイル――18か月かけて肥大化したひとつの Express ルーターです。あらゆるドメインのエンドポイントが src/routes/index.ts にすべて集約されていて、ユーザー登録の隣に決済の Webhook、その隣に商品検索が並んでいます。インラインの認可・認証チェックは 40 個のハンドラーにコピペされていて、誰も触りたがりません。注文ロジックを変更すると、300 行上にあるユーザー用エンドポイントが壊れるかもしれないからです。ファイルの先頭は、たいていこんな感じになっています。src/routes/index.ts (before — 2,000 lines)
import { Router } from "express";
import { db } from "../db";
import { stripe } from "../lib/stripe";
import { sendEmail } from "../lib/email";
import { logger } from "../lib/logger";
const router = Router();
// ---- 認証ミドルウェア(各所にコピー&ペースト)----
const requireAuth = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "Unauthorized" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: "Invalid token" });
}
};
// ---- ユーザールート ----
router.post("/users/register", async (req, res) => { /* 45 lines */ });
router.post("/users/login", async (req, res) => { /* 30 lines */ });
router.get("/users/:id", requireAuth, async (req, res) => { /* 25 lines */ });
router.put("/users/:id", requireAuth, async (req, res) => { /* 40 lines */ });
// ---- 商品ルート ----
router.get("/products", async (req, res) => { /* 60 lines */ });
router.get("/products/:id", async (req, res) => { /* 35 lines */ });
router.post("/products", requireAuth, async (req, res) => { /* 50 lines */ });
// ---- 注文ルート ----
router.post("/orders", requireAuth, async (req, res) => { /* 80 lines */ });
router.get("/orders/:id", requireAuth, async (req, res) => { /* 40 lines */ });
router.post("/orders/:id/refund", requireAuth, async (req, res) => { /* 55 lines */ });
// ---- 決済ルート ----
router.post("/payments/webhook", async (req, res) => { /* 90 lines */ });
router.get("/payments/:id/status", requireAuth, async (req, res) => { /* 30 lines */ });
// ... 混在したハンドラー、インラインバリデーション、
// 重複した認証チェックがさらに1,400行続く
export default router;
ターゲットとする構成をどのような形にしたいのかを、Devin に正確に伝えてください。 規約でDevinをガイドする
Devin はコードベースを読み込んでパターンを推論しますが、リファクタリングの場面では Knowledge のエントリが最も効果を発揮します。Devin に従わせたい規約について、エントリを追加します:
- ルーターパターン — “各ドメインルーターは
Router() を使用し、アプリケーションのルートで app.use('/domain', domainRouter) としてマウントする”
- ミドルウェア — “認証ミドルウェアは
src/middleware/ に配置し、常にインポートして使用し、その場で定義しない”
- エラーハンドリング — “すべてのルートハンドラーは
src/lib/asyncHandler.ts の asyncHandler ラッパーを使用し、生の try/catch は使わない”
コードベース内ですでによく構造化されたルーターを Devin に参照させる方が、規約を一から説明するよりも良い結果につながることが多いです。プロンプトに “src/routes/admin.ts のパターンに従ってください。このファイルはすでにきれいに分離されています” のような一文を追加してください。Devin に Knowledge エントリを生成するよう依頼することもできます。規約を説明するだけで、確認して保存できる、よく構造化されたエントリを作成します。 Devin の PR をレビューする
Devin はすべてのエンドポイントをマッピングし、インポートグラフをトレースし、共通ロジックを抽出し、ドメインファイルを作成し、ルートルーターを組み替えたうえで、テストスイートを実行します。一般的な PR は次のようになります。refactor: Split monolithic router into domain-specific route files
Files changed (8):
src/routes/users.ts — 4 endpoints, auth middleware imported
src/routes/products.ts — 3 endpoints, public + auth-protected
src/routes/orders.ts — 3 endpoints, all auth-protected
src/routes/payments.ts — 2 endpoints, webhook + status check
src/routes/index.ts — root router mounting all domains
src/middleware/auth.ts — requireAuth, requireAdmin (extracted)
src/middleware/validate.ts — validateBody schema middleware
Old src/routes/index.ts — 2,000-line monolith replaced
All 112 API tests pass. No URL changes.
分割後のクリーンなルートルーターは次のとおりです。src/routes/index.ts (after — 15 lines)
import { Router } from "express";
import usersRouter from "./users";
import productsRouter from "./products";
import ordersRouter from "./orders";
import paymentsRouter from "./payments";
const router = Router();
router.use("/users", usersRouter);
router.use("/products", productsRouter);
router.use("/orders", ordersRouter);
router.use("/payments", paymentsRouter);
export default router;
また、共有ミドルウェアを正しくインポートしたドメインルートファイルは次のとおりです:src/routes/orders.ts (after — excerpt)
import { Router } from "express";
import { requireAuth } from "../middleware/auth";
import { validateBody } from "../middleware/validate";
import { createOrderSchema, refundSchema } from "../schemas/orders";
import { db } from "../db";
const router = Router();
router.post("/", requireAuth, validateBody(createOrderSchema),
async (req, res) => {
const order = await db.orders.create({
userId: req.user.id,
items: req.body.items,
total: req.body.total,
});
res.status(201).json(order);
}
);
router.get("/:id", requireAuth, async (req, res) => {
const order = await db.orders.findByPk(req.params.id);
if (!order) return res.status(404).json({ error: "Order not found" });
res.json(order);
});
router.post("/:id/refund", requireAuth, validateBody(refundSchema),
async (req, res) => {
// モノリスからきれいに抽出された返金ロジック
}
);
export default router;
すべてのURLパスは同一のままです — /orders は /orders にマウントされた ordersRouter によって処理されるため、既存のクライアントやテストは変更なしでそのまま動作します。 (任意)ブランチをチェックアウトしてローカル環境でテストする
このような構造的なリファクタリングでは、マージ前にブランチをローカルに pull して検証しておく価値があります。Windsurf や使い慣れた IDE で開き、アプリを起動していくつかのエンドポイントを叩き、ルーティング、ミドルウェア、エラー処理が以前と同じように動作することを確認してください。git fetch origin && git checkout devin/refactor-router-split
npm install && npm test
npm run dev
# いくつかのエンドポイントにアクセス: curl http://localhost:3000/users, /orders, /payments
何か気になる点があれば、PR にコメントを残してください — Devin がそれを拾い上げて修正を反映します。 クリーンアップを続ける
ルーターの分割が完了したら、フォローアッププロンプトを使ってリファクタリングをさらに進めてください。