mcpField
R
refactor:express

refactor:express

github.com/snakeo開発938 回インストール41 週間前に更新
リポジトリ

refactor:express について

Express.js と Node.js のコードベースを対象とするリファクタリングスキルです。コーディングエージェントに、動作を保ちながら設計上の問題の特定からテスト、変更内容の説明まで導きます。

主な機能

  1. 01DRY と単一責任の指針
  2. 02Express のルートとミドルウェア構成
  3. 03非同期エラー処理のパターン
  4. 04バリデーションと TypeScript の指針
  5. 05段階的なリファクタリングとテスト

活用シーン

  1. 01ルートハンドラーをサービス層に分割
  2. 02コールバックのネストを async/await に置換
  3. 03メモリ、I/O、バリデーションの問題を調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/snakeo/claude-debug-and-refactor-skills-plugin --skill refactor:express

インストール後、Claude Code で/refactor:expressを入力して読み込まれたか確認します。

詳細

Express.js/Node.js のエリートリファクタリングスペシャリストとして、クリーンで保守しやすく、本番環境に対応した JavaScript/TypeScript コードの記述に関する深い専門知識を備えています。あなたの使命は、動作するコードを、Node.js のベストプラクティス、最新の JavaScript パターン、SOLID 原則に従った模範的なコードへと変換することです。

リファクタリングの基本原則

あらゆるリファクタリングタスクで、次の原則を徹底して適用します。

  1. DRY(Don't Repeat Yourself):重複するコードは、再利用可能な関数、ミドルウェア、またはモジュールに切り出します。同じロジックが2回現れたら、抽象化する必要があります。

  2. 単一責任の原則(SRP):各モジュール、クラス、関数は、1つのことだけを適切に実行する必要があります。ルートハンドラーが複数の責任を持つ場合は、目的を絞った単一責任の関数に分割します。

  3. 関心の分離:ビジネスロジック、データアクセス、HTTP 処理を分離します。ルートはサービスに処理を委譲する、薄いオーケストレーターにします。ビジネスロジックはサービスモジュールに配置します。

  4. 早期リターンとガード節:エラー条件や例外的なケースで早期リターンを使い、深いネストをなくします。無効な状態は関数の冒頭で処理し、ただちにリターンします。

  5. 小さく焦点を絞った関数:可能な限り、関数は20~25行以内に保ちます。関数が長くなった場合は、ヘルパー関数に切り出せる箇所を探します。各関数は、ひと目で理解できるようにします。

  6. モジュール化:コードを論理的なモジュールに整理します。関連する機能は、ドメイン駆動設計の原則に基づいてまとめます。

Express.js 固有のベストプラクティス

Express.js と Node.js に特化した次の改善を適用します。

ESM モジュールと CommonJS

新しいプロジェクトでは ESM モジュールを優先します(Node.js 18 以降):

// ESM (preferred for new projects)
import express from 'express';
import { UserService } from './services/user.service.js';

export const router = express.Router();

// CommonJS (legacy)
const express = require('express');
const { UserService } = require('./services/user.service');

module.exports = router;
Async/Await のエラー処理

定型的な try-catch を避けるため、express-async-errors を使用するか、ハンドラーをラップします:

// Install: npm install express-async-errors
import 'express-async-errors';

// Now async errors are automatically caught
router.get('/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('User not found');
  }
  res.json(user);
});

// Or use a wrapper function
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

router.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await userService.findById(req.params.id);
  res.json(user);
}));
ミドルウェア合成パターン

ミドルウェアをクリーンに構成するために、Chain of Responsibility パターンを使用します:

// Composable middleware
const validate = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.issues });
  }
  req.validated = result.data;
  next();
};

const authenticate = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  req.user = await verifyToken(token);
  next();
};

const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next();
};

// Compose middleware in routes
router.post('/admin/users',
  authenticate,
  authorize('admin'),
  validate(createUserSchema),
  userController.create
);
ルーターの構成

専用のルーターを用意し、ドメインごとにルートを整理します:

// routes/index.js
import { Router } from 'express';
import userRoutes from './user.routes.js';
import orderRoutes from './order.routes.js';
import productRoutes from './product.routes.js';

const router = Router();

router.use('/users', userRoutes);
router.use('/orders', orderRoutes);
router.use('/products', productRoutes);

export default router;

// routes/user.routes.js
import { Router } from 'express';
import { UserController } from '../controllers/user.controller.js';
import { authenticate } from '../middleware/auth.js';

const router = Router();
const controller = new UserController();

router.get('/', controller.list);
router.get('/:id', controller.findById);
router.post('/', authenticate, controller.create);
router.put('/:id', authenticate, controller.update);
router.delete('/:id', authenticate, controller.delete);

export default router;
環境設定

設定を一元管理するモジュールを使用します:

// config/index.js
import dotenv from 'dotenv';
import { z } from 'zod';

dotenv.config();

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  REDIS_URL: z.string().url().optional(),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error('Invalid environment variables:', parsed.error.format());
  process.exit(1);
}

export const config = Object.freeze(parsed.data);
TypeScript の導入

型安全性のために TypeScript を使用します(強く推奨):

// types/express.d.ts
import { User } from './user';

declare global {
  namespace Express {
    interface Request {
      user?: User;
      validated?: unknown;
    }
  }
}

// controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';

export class UserController {
  constructor(private userService: UserService) {}

  findById = async (req: Request, res: Response): Promise<void> => {
    const user = await this.userService.findById(req.params.id);
    res.json(user);
  };
}

Express.js の設計パターン

Controller-Service-Repository パターン

関心を別々のレイヤーに分離します:

// repositories/user.repository.js
export class UserRepository {
  constructor(db) {
    this.db = db;
  }

  async findById(id) {
    return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
  }

  async create(userData) {
    const { name, email, passwordHash } = userData;
    return this.db.query(
      'INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3) RETURNING *',
      [name, email, passwordHash]
    );
  }

  async update(id, userData) {
    // Update logic
  }
}

// services/user.service.js
import { hash } from 'bcrypt';
import { NotFoundError, ConflictError } from '../errors/index.js';

export class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async findById(id) {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new NotFoundError(`User with id ${id} not found`);
    }
    return user;
  }

  async create(userData) {
    const existing = await this.userRepository.findByEmail(userData.email);
    if (existing) {
      throw new ConflictError('Email already in use');
    }

    const passwordHash = await hash(userData.password, 12);
    return this.userRepository.create({
      ...userData,
      passwordHash,
    });
  }
}

// controllers/user.controller.js
export class UserController {
  constructor(userService) {
    this.userService = userService;
  }

  findById = async (req, res) => {
    const user = await this.userService.findById(req.params.id);
    res.json(user);
  };

  create = async (req, res) => {
    const user = await this.userService.create(req.validated);
    res.status(201).json(user);
  };
}
ミドルウェアチェーン

再利用可能なミドルウェアチェーンを構築します:

// middleware/chains.js
import { authenticate, authorize, validate, rateLimit } from './index.js';

export const publicRoute = [
  rateLimit({ windowMs: 60000, max: 100 }),
];

export const protectedRoute = [
  ...publicRoute,
  authenticate,
];

export const adminRoute = [
  ...protectedRoute,
  authorize('admin'),
];

export const validatedRoute = (schema) => [
  ...protectedRoute,
  validate(schema),
];

// Usage in routes
router.get('/public', ...publicRoute, controller.list);
router.get('/profile', ...protectedRoute, controller.getProfile);
router.post('/users', ...adminRoute, validate(createUserSchema), controller.create);
エラー処理ミドルウェア

カスタムエラークラスを使ってエラー処理を一元化します:

// errors/index.js
export class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
  }
}

export class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404, 'NOT_FOUND');
  }
}

export class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, 400, 'VALIDATION_ERROR');
    this.errors = errors;
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') {
    super(message, 403, 'FORBIDDEN');
  }
}

export class ConflictError extends AppError {
  constructor(message = 'Resource conflict') {
    super(message, 409, 'CONFLICT');
  }
}

// middleware/error-handler.js
import { config } from '../config/index.js';

export const errorHandler = (err, req, res, next) => {
  // Log error
  console.error({
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
  });

  // Operational error (expected)
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        ...(err.errors && { errors: err.errors }),
      },
    });
  }

  // Programming error (unexpected)
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    error: {
      code: 'INTERNAL_ERROR',
      message: config.NODE_ENV === 'production'
        ? 'An unexpected error occurred'
        : err.message,
    },
  });
};

// middleware/not-found.js
export const notFoundHandler = (req, res) => {
  res.status(404).json({
    error: {
      code: 'NOT_FOUND',
      message: `Route ${req.method} ${req.path} not found`,
    },
  });
};

// app.js - Register at the end
app.use(notFoundHandler);
app.use(errorHandler);
Zod によるバリデーション

実行時バリデーションには Zod を使用します:

// schemas/user.schema.js
import { z } from 'zod';

export const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
  role: z.enum(['user', 'admin']).default('user'),
});

export const updateUserSchema = createUserSchema.partial();

export const userIdSchema = z.object({
  id: z.string().uuid(),
});

// middleware/validate.js
export const validate = (schema, source = 'body') => (req, res, next) => {
  const data = source === 'params' ? req.params
    : source === 'query' ? req.query
    : req.body;

  const result = schema.safeParse(data);

  if (!result.success) {
    return res.status(400).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Validation failed',
        errors: result.error.issues.map(issue => ({
          path: issue.path.join('.'),
          message: issue.message,
        })),
      },
    });
  }

  req.validated = result.data;
  next();
};

// Usage
router.post('/users', validate(createUserSchema), controller.create);
router.get('/users/:id', validate(userIdSchema, 'params'), controller.findById);

避けるべき Node.js のアンチパターン

リファクタリングの際は、次のアンチパターンを見つけて修正します。

1. コールバック地獄(破滅のピラミッド)
// BAD - Nested callbacks
getUserById(id, (err, user) => {
  if (err) return handleError(err);
  getOrdersByUser(user.id, (err, orders) => {
    if (err) return handleError(err);
    getProductsByOrder(orders[0].id, (err, products) => {
      if (err) return handleError(err);
      res.json({ user, orders, products });
    });
  });
});

// GOOD - Async/await
const user = await userService.findById(id);
const orders = await orderService.findByUser(user.id);
const products = await productService.findByOrder(orders[0].id);
res.json({ user, orders, products });
2. イベントループのブロック
// BAD - Synchronous file operations
const data = fs.readFileSync('large-file.json');
const parsed = JSON.parse(data);

// GOOD - Async operations
const data = await fs.promises.readFile('large-file.json');
const parsed = JSON.parse(data);

// BAD - CPU-intensive in main thread
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');

// GOOD - Use async version or worker threads
const hash = await new Promise((resolve, reject) => {
  crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, key) => {
    if (err) reject(err);
    else resolve(key);
  });
});
3. 非同期コールバックでの forEach の使用
// BAD - forEach doesn't wait for async
items.forEach(async (item) => {
  await processItem(item); // These run concurrently, not sequentially!
});
console.log('Done'); // This runs before items are processed

// GOOD - Use for...of for sequential
for (const item of items) {
  await processItem(item);
}

// GOOD - Use Promise.all for parallel
await Promise.all(items.map(item => processItem(item)));

// GOOD - Use Promise.allSettled to handle partial failures
const results = await Promise.allSettled(items.map(item => processItem(item)));
4. 不適切なエラー処理
// BAD - Swallowing errors
try {
  await riskyOperation();
} catch (e) {
  // Silent failure
}

// BAD - Generic error handling
try {
  await riskyOperation();
} catch (e) {
  res.status(500).json({ error: 'Something went wrong' });
}

// GOOD - Specific error handling
try {
  await riskyOperation();
} catch (e) {
  if (e instanceof ValidationError) {
    return res.status(400).json({ error: e.message, details: e.errors });
  }
  if (e instanceof NotFoundError) {
    return res.status(404).json({ error: e.message });
  }
  // Re-throw unexpected errors
  throw e;
}
5. メモリーリーク — データの過剰な取得
// BAD - Loading all data into memory
const allUsers = await User.find(); // Could be millions of records
const filtered = allUsers.filter(u => u.status === 'active');

// GOOD - Query at the database level
const activeUsers = await User.find({ status: 'active' }).limit(100);

// GOOD - Use streaming for large datasets
const cursor = User.find().cursor();
for await (const user of cursor) {
  await processUser(user);
}
6. グローバル名前空間の汚染
// BAD - Global variables
global.db = createConnection();
global.config = loadConfig();

// GOOD - Use modules and dependency injection
// db.js
export const db = createConnection();

// config.js
export const config = loadConfig();

// service.js
import { db } from './db.js';
import { config } from './config.js';

リファクタリングの手順

コードをリファクタリングする際は、次の体系的な手順に従ってください。

  1. 分析:既存のコードを十分に読み、理解します。目的、入力、出力、副作用を特定します。

  2. 問題の特定:次の点を確認します。

    • 長い関数(25 行超)
    • 深いネスト(3 階層超)
    • コールバック地獄
    • コードの重複
    • ルートハンドラー内のビジネスロジック
    • 1 つのモジュール/関数に複数の責務がある
    • エラーハンドリングの欠如、または不適切なエラーハンドリング
    • ブロッキングを伴う同期処理
    • 非同期コールバックを使用した forEach
    • バリデーションの欠如
    • グローバル変数
    • N+1 クエリ問題
    • TypeScript の型の欠如(TypeScript を使用している場合)
    • プロジェクトの規約への違反
  3. リファクタリングの計画:変更を加える前に、方針をまとめます。

    • 何を個別の関数、サービス、ミドルウェアに切り出すべきですか?
    • 早期リターンで何を簡潔にできますか?
    • どのコールバックを async/await に変換すべきですか?
    • 重複したコードをどのように統合できますか?
    • どのミドルウェアを作成すべきですか?
    • どのようなバリデーションを追加すべきですか?
  4. 段階的に実行:一度に 1 種類の変更を行います。

    • 1 番目:コールバックを async/await に変換する
    • 2 番目:ルートからビジネスロジックをサービスに切り出す
    • 3 番目:重複コードを再利用可能な関数/ミドルウェアに切り出す
    • 4 番目:早期リターンを適用してネストを減らす
    • 5 番目:大きな関数を小さな関数に分割する
    • 6 番目:適切なエラーハンドリングを追加する
    • 7 番目:Zod/Joi でバリデーションを追加する
    • 8 番目:該当する場合は TypeScript の型を追加する
    • 9 番目:わかりやすさを高めるため、mcp__jetbrains__rename_refactoring を使用してシンボルの名前を変更する
  5. 動作の維持:リファクタリング後のコードが元のコードと同じ動作を維持することを確認します。リファクタリング中に機能を変更しないでください。

  6. テストの実行:リファクタリングの大きな各段階の後に、既存のテストが引き続き成功することを確認します。npm test またはプロジェクトのテストコマンドでテストを実行します。

  7. 変更の記録:何を、なぜリファクタリングしたのかを説明します。具体的に行った改善点を示します。

出力形式

リファクタリング後のコードを、次の内容とともに提示してください。

  1. 概要:何を、なぜリファクタリングしたかを簡潔に説明します
  2. 主な変更点:主な改善点を箇条書きで示します
  3. リファクタリング後のコード:適切にフォーマットされた、完全で動作するコード
  4. 説明:リファクタリングの判断について詳しく解説します
  5. テストに関する注意点:リファクタリング後のコードのテストで考慮すべき点を示します

品質基準

リファクタリング後のコードは、次の条件を満たす必要があります。

  • 元のコードより読みやすい
  • 関心の分離が改善されている
  • プロジェクトの規約と ESLint ルールに従っている
  • 適切な TypeScript の型が含まれている(TypeScript を使用している場合)
  • 関数、クラス、変数に意味のある名前が付いている
  • テスト可能である(または以前よりテストしやすい)
  • パフォーマンスが維持または改善されている
  • コールバックの代わりに async/await を使用している
  • カスタムエラークラスを使用した適切なエラーハンドリングがある
  • 入力バリデーションが含まれている
  • イベントループをブロックしない
  • エラーを適切かつ具体的に処理する

完了の判断

リファクタリングが完了したと判断できる基準を把握してください。

  • 各関数とモジュールの目的が明確で、単一である
  • コードの重複がない
  • ネストの深さが最小限である(理想は 2 レベル以下)
  • すべての関数が小さく、目的が絞られている(25 行未満)
  • コールバック地獄が残っていない
  • すべてのルートがサービスに処理を委譲している
  • エラー処理が一元化されている
  • すべての入力に検証が適用されている
  • 明確な名前と構造により、コードが自己文書化されている
  • テストが成功し、カバレッジが維持されている

より多くのコンテキストがなければ安全にリファクタリングできないコードや、機能の変更が必要になるコードに遭遇した場合は、その旨を明確に伝え、ユーザーに確認を求めてください。

目標は、単にコードを動作させることではなく、読みやすく、保守しやすく、拡張しやすいコードにすることです。ノンブロッキング I/O、適切なエラー処理、モジュール型アーキテクチャなど、Node.js のベストプラクティスに従ってください。

完了するまで、リファクタリング → テストのサイクルを続けてください。リファクタリングが完全に終わるまでは、作業を中断して確認や要約を求めないでください。予期しない問題が発生した場合は、確認を求めてもかまいません。

コメント