mcpField
E
express

express

github.com/blencorp開発11198 回インストール49 週間前に更新
リポジトリ

express について

バックエンド API を構築・保守する開発者向けに、Express.js の実践パターンをまとめたスキルです。ルートとコントローラーの分離、中間ウェアの構成、リクエストやエラーの処理を扱います。

主な機能

  1. 01ルートとコントローラーの分離
  2. 02認証・監査ミドルウェアのパターン
  3. 03ミドルウェアの順序ガイド
  4. 04リクエストとレスポンスの型付け
  5. 05一貫したエラー処理パターン

活用シーン

  1. 01バックエンド API のルート構成
  2. 02認証やエラーミドルウェアの追加
  3. 03API レスポンスの処理と型付け

インストール

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

$npx skills add https://github.com/blencorp/claude-code-kit --skill express

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

同梱ファイルskill-rules-fragment.json

詳細

Express.js フレームワークのパターン

目的

スケーラブルなバックエンド API を構築するために不可欠な Express.js のパターンです。クリーンなルーティング、ミドルウェアの合成、適切なリクエスト/レスポンス処理に重点を置いています。

このスキルを使用する場面

  • Express のルートを作成または変更するとき
  • ミドルウェア(認証、検証、エラー処理)を構築するとき
  • Express の Request/Response オブジェクトを扱うとき
  • BaseController パターンを実装するとき
  • Express でエラーを処理するとき

クリーンなルートのパターン

ルート専用ルート

ルートで行うこと:

  • ✅ ルートパスを定義する
  • ✅ ミドルウェアを登録する
  • ✅ コントローラーに処理を委譲する

ルートで行わないこと:

  • ❌ ビジネスロジックを含める
  • ❌ データベースに直接アクセスする
  • ❌ 検証ロジックを実装する
  • ❌ 複雑なレスポンスを整形する
import { Router } from 'express';
import { UserController } from '../controllers/UserController';
import { SSOMiddlewareClient } from '../middleware/SSOMiddleware';

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

// Clean delegation - no business logic
router.get('/:id',
    SSOMiddlewareClient.verifyLoginStatus,
    async (req, res) => controller.getUser(req, res)
);

router.post('/',
    SSOMiddlewareClient.verifyLoginStatus,
    async (req, res) => controller.createUser(req, res)
);

export default router;

BaseController パターン

実装
import * as Sentry from '@sentry/node';
import { Response } from 'express';

export abstract class BaseController {
    protected handleError(
        error: unknown,
        res: Response,
        context: string,
        statusCode = 500
    ): void {
        Sentry.withScope((scope) => {
            scope.setTag('controller', this.constructor.name);
            scope.setTag('operation', context);
            Sentry.captureException(error);
        });

        res.status(statusCode).json({
            success: false,
            error: {
                message: error instanceof Error ? error.message : 'An error occurred',
                code: statusCode,
            },
        });
    }

    protected handleSuccess<T>(
        res: Response,
        data: T,
        message?: string,
        statusCode = 200
    ): void {
        res.status(statusCode).json({
            success: true,
            message,
            data,
        });
    }

    protected async withTransaction<T>(
        name: string,
        operation: string,
        callback: () => Promise<T>
    ): Promise<T> {
        return await Sentry.startSpan({ name, op: operation }, callback);
    }

    protected addBreadcrumb(
        message: string,
        category: string,
        data?: Record<string, any>
    ): void {
        Sentry.addBreadcrumb({ message, category, level: 'info', data });
    }
}
BaseController の使用
import { Request, Response } from 'express';
import { BaseController } from './BaseController';
import { UserService } from '../services/userService';
import { createUserSchema } from '../validators/userSchemas';

export class UserController extends BaseController {
    private userService: UserService;

    constructor() {
        super();
        this.userService = new UserService();
    }

    async getUser(req: Request, res: Response): Promise<void> {
        try {
            this.addBreadcrumb('Fetching user', 'user_controller', {
                userId: req.params.id
            });

            const user = await this.userService.findById(req.params.id);

            if (!user) {
                return this.handleError(
                    new Error('User not found'),
                    res,
                    'getUser',
                    404
                );
            }

            this.handleSuccess(res, user);
        } catch (error) {
            this.handleError(error, res, 'getUser');
        }
    }

    async createUser(req: Request, res: Response): Promise<void> {
        try {
            const validated = createUserSchema.parse(req.body);

            const user = await this.withTransaction(
                'user.create',
                'db.query',
                () => this.userService.create(validated)
            );

            this.handleSuccess(res, user, 'User created successfully', 201);
        } catch (error) {
            this.handleError(error, res, 'createUser');
        }
    }
}

ミドルウェアのパターン

認証
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../config/unifiedConfig';

export class SSOMiddlewareClient {
    static verifyLoginStatus(req: Request, res: Response, next: NextFunction): void {
        const token = req.cookies.refresh_token;

        if (!token) {
            return res.status(401).json({ error: 'Not authenticated' });
        }

        try {
            const decoded = jwt.verify(token, config.tokens.jwt);
            res.locals.claims = decoded;
            res.locals.effectiveUserId = decoded.sub;
            next();
        } catch (error) {
            res.status(401).json({ error: 'Invalid token' });
        }
    }
}
AsyncLocalStorage による監査
import { Request, Response, NextFunction } from 'express';
import { AsyncLocalStorage } from 'async_hooks';
import { v4 as uuidv4 } from 'uuid';

export interface AuditContext {
    userId: string;
    userName?: string;
    requestId: string;
    timestamp: Date;
}

export const auditContextStorage = new AsyncLocalStorage<AuditContext>();

export function auditMiddleware(req: Request, res: Response, next: NextFunction): void {
    const context: AuditContext = {
        userId: res.locals.effectiveUserId || 'anonymous',
        userName: res.locals.claims?.preferred_username,
        timestamp: new Date(),
        requestId: req.id || uuidv4(),
    };

    auditContextStorage.run(context, () => next());
}

export function getAuditContext(): AuditContext | null {
    return auditContextStorage.getStore() || null;
}
エラーバウンダリー
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';

export function errorBoundary(
    error: Error,
    req: Request,
    res: Response,
    next: NextFunction
): void {
    const statusCode = error.statusCode || 500;

    Sentry.captureException(error);

    res.status(statusCode).json({
        success: false,
        error: {
            message: error.message,
            code: error.name,
        },
    });
}

// Async wrapper
export function asyncErrorWrapper(
    handler: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
    return async (req: Request, res: Response, next: NextFunction) => {
        try {
            await handler(req, res, next);
        } catch (error) {
            next(error);
        }
    };
}

ミドルウェアの順序

重要な順序
import express from 'express';
import * as Sentry from '@sentry/node';

const app = express();

// 1. Sentry request handler (FIRST)
app.use(Sentry.Handlers.requestHandler());

// 2. Body/cookie parsing
app.use(express.json());
app.use(cookieParser());

// 3. Routes
app.use('/api/users', userRoutes);

// 4. Error handler (AFTER routes)
app.use(errorBoundary);

// 5. Sentry error handler (LAST)
app.use(Sentry.Handlers.errorHandler());

ルール:

  • Sentry のリクエストハンドラーを最初に配置する
  • ボディ/Cookie パーサーはルートより前に配置する
  • エラーハンドラーはすべてのルートの後に配置する
  • Sentry のエラーハンドラーを最後に配置する

リクエスト/レスポンスの処理

型付きリクエスト
interface CreateUserRequest {
    email: string;
    name: string;
    password: string;
}

async function createUser(
    req: Request<{}, {}, CreateUserRequest>,
    res: Response
): Promise<void> {
    const { email, name, password } = req.body; // Typed
}
レスポンスのパターン
// Success (200)
res.json({ success: true, data: user });

// Created (201)
res.status(201).json({ success: true, data: user });

// Error (400/500)
res.status(400).json({ success: false, error: { message: 'Invalid input' } });
HTTP ステータスコード
コード使用例
200成功(GET、PUT)
201作成済み(POST)
204コンテンツなし(DELETE)
400不正なリクエスト
401認証されていない
403禁止されている
404見つからない
500サーバーエラー

よくある間違い

1. ルート内のビジネスロジック
// ❌ Never do this
router.post('/submit', async (req, res) => {
    // 100+ lines of logic
    const user = await db.user.create(req.body);
    const workflow = await processWorkflow(user);
    res.json(workflow);
});

// ✅ Do this
router.post('/submit', (req, res) => controller.submit(req, res));
2. ミドルウェアの順序が誤っている
// ❌ Error handler before routes
app.use(errorBoundary);
app.use('/api', routes); // Won't catch errors

// ✅ Error handler after routes
app.use('/api', routes);
app.use(errorBoundary);
3. エラー処理がない
// ❌ Unhandled errors crash server
router.get('/user/:id', async (req, res) => {
    const user = await userService.get(req.params.id); // May throw
    res.json(user);
});

// ✅ Proper error handling
async getUser(req: Request, res: Response): Promise<void> {
    try {
        const user = await this.userService.get(req.params.id);
        this.handleSuccess(res, user);
    } catch (error) {
        this.handleError(error, res, 'getUser');
    }
}

よく使用するインポート

// Express core
import express, { Request, Response, NextFunction, Router } from 'express';

// Middleware
import cookieParser from 'cookie-parser';
import cors from 'cors';

// Sentry
import * as Sentry from '@sentry/node';

// Utilities
import { AsyncLocalStorage } from 'async_hooks';

ベストプラクティス

  1. ルートをクリーンに保つ - ルート処理に徹し、コントローラーに委譲します
  2. BaseController を使用する - 一貫性のあるエラー処理とレスポンスの整形を実現します
  3. ミドルウェアを適切な順序で配置する - Sentry → パーサー → ルート → エラーハンドラー
  4. すべてに型を付ける - Request/Response の型には TypeScript を使用します
  5. すべてのエラーを処理する - コントローラーでは try-catch を使用し、全体にエラーバウンダリーを設けます

関連スキル:

  • nodejs - Node.js のコアパターンと非同期処理
  • backend-dev-guidelines - バックエンドアーキテクチャの完全ガイド
  • prisma - Prisma ORM を使用したデータベースのパターン

コメント