Agent Skills by ALSEL
Anthropic Claudeソフトウェア開発⭐ リポ 299品質スコア 89/100

express-api-patterns

Express.jsを使用したAPI開発、ルーティング、ミドルウェア、エラーハンドリング、リクエスト検証、CORSの設定ができます。Expressのルート構築、ミドルウェアの実装、APIリクエストの処理、バックエンドサーバーのセットアップが必要な場合に利用します。

description の原文を見る

Express.js API development, route handling, middleware, error handling, request validation, CORS. Use when building Express routes, implementing middleware, handling API requests, or setting up the backend server.

SKILL.md 本文

Express APIパターン

基本原則

  1. RESTful設計 - HTTPメソッドを適切に使用する(GET、POST、PUT、DELETE)
  2. ミドルウェアファースト - クロスカッティングな関心事にはミドルウェアを使用する
  3. エラーハンドリング - 集約されたエラーハンドリングミドルウェア
  4. 検証 - 処理前にすべての入力を検証する
  5. セキュリティ - CORS、レート制限、入力サニタイゼーション

サーバーセットアップパターン

正解: よく構造化されたExpressサーバー

// server/index.js
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';

// Load environment variables
dotenv.config();

// Import routes
import authRoutes from './routes/auth.js';
import generateRoutes from './routes/generate.js';
import imageRoutes from './routes/images.js';

const app = express();
const PORT = process.env.PORT || 3001;

// ===== Middleware =====

// CORS configuration
app.use(cors({
  origin: process.env.CLIENT_URL || 'http://localhost:5173',
  credentials: true
}));

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Request logging (development only)
if (process.env.NODE_ENV === 'development') {
  app.use((req, res, next) => {
    console.log(`${req.method} ${req.path}`);
    next();
  });
}

// ===== Routes =====

app.use('/api/auth', authRoutes);
app.use('/api/generate', generateRoutes);
app.use('/api/images', imageRoutes);

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// ===== Error Handling =====

// 404 handler
app.use((req, res) => {
  res.status(404).json({ error: 'Endpoint not found' });
});

// Global error handler
app.use((err, req, res, next) => {
  console.error('Error:', err);

  const status = err.status || 500;
  const message = err.message || 'Internal server error';

  res.status(status).json({ error: message });
});

// ===== Start Server =====

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
  console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
});

export default app;

ルートパターン

正解: よく構造化されたルート

// server/routes/generate.js
import express from 'express';
import { generatePage, generatePageStream } from '../services/claude.js';
import fs from 'fs/promises';
import path from 'path';

const router = express.Router();

// Load system prompt
let systemPrompt = '';
try {
  systemPrompt = await fs.readFile(
    path.join(process.cwd(), 'prompts', 'system.txt'),
    'utf-8'
  );
} catch (error) {
  console.error('Failed to load system prompt:', error);
}

/**
 * POST /api/generate
 * Generate or update instructional page
 */
router.post('/', async (req, res, next) => {
  try {
    // 1. Extract and validate input
    const { config, message, history = [] } = req.body;

    if (!config || !config.topic) {
      return res.status(400).json({ error: 'Topic is required' });
    }

    if (!message) {
      return res.status(400).json({ error: 'Message is required' });
    }

    if (config.depthLevel < 0 || config.depthLevel > 4) {
      return res.status(400).json({ error: 'Depth level must be 0-4' });
    }

    // 2. Call service
    const result = await generatePage(systemPrompt, config, message, history);

    // 3. Return response
    res.json({
      message: result.message,
      html: result.html,
      timestamp: new Date().toISOString()
    });

  } catch (error) {
    // Pass to error handler
    next(error);
  }
});

/**
 * POST /api/generate/stream
 * Generate page with streaming response
 */
router.post('/stream', async (req, res, next) => {
  try {
    const { config, message, history = [] } = req.body;

    // Validation (same as above)
    if (!config?.topic || !message) {
      return res.status(400).json({ error: 'Invalid request' });
    }

    // Set headers for Server-Sent Events
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    // Stream generation
    await generatePageStream(systemPrompt, config, message, history, (chunk) => {
      res.write(`data: ${JSON.stringify(chunk)}\n\n`);
    });

    res.end();

  } catch (error) {
    next(error);
  }
});

export default router;

不正解: 不十分なルート構造

// ❌ これをしてはいけません
router.post('/generate', (req, res) => {
  // ❌ 入力検証なし
  // ❌ エラーハンドリングなし
  // ❌ チェックなしでネストされたプロパティに直接アクセス
  generatePage(req.body.config.topic, req.body.message).then(result => {
    res.send(result); // ❌ res.json()を使用していない
  });
});

ミドルウェアパターン

認証ミドルウェア

// server/middleware/auth.js
export const verifyPassword = (req, res, next) => {
  const { password } = req.body;
  const correctPassword = process.env.FACULTY_PASSWORD;

  if (!correctPassword) {
    return res.status(500).json({ error: 'Server configuration error' });
  }

  if (password !== correctPassword) {
    return res.status(401).json({ error: 'Invalid password' });
  }

  next(); // Password correct, proceed
};

// Usage in route
import { verifyPassword } from '../middleware/auth.js';

router.post('/verify', verifyPassword, (req, res) => {
  res.json({ success: true });
});

リクエスト検証ミドルウェア

// server/middleware/validate.js
export const validateGenerateRequest = (req, res, next) => {
  const { config, message } = req.body;
  const errors = [];

  if (!config) {
    errors.push('config is required');
  } else {
    if (!config.topic) errors.push('config.topic is required');
    if (config.depthLevel === undefined) errors.push('config.depthLevel is required');
    if (config.depthLevel < 0 || config.depthLevel > 4) {
      errors.push('config.depthLevel must be 0-4');
    }
  }

  if (!message) {
    errors.push('message is required');
  }

  if (errors.length > 0) {
    return res.status(400).json({ error: errors.join(', ') });
  }

  next();
};

// Usage
router.post('/', validateGenerateRequest, async (req, res, next) => {
  // Request is validated
  // ... handle request
});

レート制限ミドルウェア

// server/middleware/rateLimit.js
import rateLimit from 'express-rate-limit';

export const generateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 50, // 50 requests per window
  message: { error: 'Too many requests, please try again later' },
  standardHeaders: true,
  legacyHeaders: false
});

// Usage
router.post('/', generateLimiter, async (req, res, next) => {
  // Rate limited endpoint
});

エラーハンドリングパターン

カスタムエラークラス

// server/utils/errors.js
export class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
    this.status = 400;
  }
}

export class APIError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.name = 'APIError';
    this.status = status;
  }
}

// Usage in route
import { ValidationError, APIError } from '../utils/errors.js';

router.post('/', async (req, res, next) => {
  try {
    if (!req.body.config) {
      throw new ValidationError('Config is required');
    }

    const result = await someAPICall();

    if (!result) {
      throw new APIError('API call failed', 503);
    }

    res.json(result);
  } catch (error) {
    next(error); // Pass to error handler
  }
});

集約されたエラーハンドラー

// server/middleware/errorHandler.js
export const errorHandler = (err, req, res, next) => {
  // Log error
  console.error('Error:', {
    name: err.name,
    message: err.message,
    stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
    url: req.url,
    method: req.method
  });

  // Determine status and message
  const status = err.status || 500;
  const message = err.message || 'Internal server error';

  // Send response
  res.status(status).json({
    error: message,
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
};

// In server setup
app.use(errorHandler);

サービスレイヤーパターン

ルートハンドラーからビジネスロジックを分離します:

// server/services/claude.js
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY
});

export const generatePage = async (systemPrompt, config, message, history) => {
  // Business logic here
  const messages = [
    ...history.map(msg => ({ role: msg.role, content: msg.content })),
    { role: 'user', content: buildPrompt(config, message) }
  ];

  try {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 8192,
      system: systemPrompt,
      messages: messages
    });

    return {
      message: extractMessage(response.content[0].text),
      html: extractHTML(response.content[0].text)
    };
  } catch (error) {
    throw new APIError(`Claude API error: ${error.message}`, 503);
  }
};

// Helper functions
const buildPrompt = (config, message) => {
  let prompt = message + '\n\n';
  prompt += `Topic: ${config.topic}\n`;
  prompt += `Depth Level: ${config.depthLevel}\n`;

  if (config.styleFlags?.length > 0) {
    prompt += `Style Flags: ${config.styleFlags.join(', ')}\n`;
  }

  return prompt;
};

const extractHTML = (text) => {
  const match = text.match(/```html\n([\s\S]*?)\n```/);
  if (!match) throw new Error('Could not extract HTML');
  return match[1].trim();
};

const extractMessage = (text) => {
  return text.split('```html')[0].trim();
};

ファイルアップロードパターン

// server/routes/images.js
import express from 'express';
import multer from 'multer';
import { uploadToCloudinary } from '../services/cloudinary.js';

const router = express.Router();

// Configure multer for memory storage
const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 10 * 1024 * 1024 // 10MB
  },
  fileFilter: (req, file, cb) => {
    // Only allow images
    if (!file.mimetype.startsWith('image/')) {
      return cb(new Error('Only image files allowed'));
    }
    cb(null, true);
  }
});

/**
 * POST /api/images/upload
 * Upload image to Cloudinary
 */
router.post('/upload', upload.single('image'), async (req, res, next) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file uploaded' });
    }

    // Upload to Cloudinary
    const result = await uploadToCloudinary(req.file.buffer);

    res.json({
      url: result.secure_url,
      publicId: result.public_id
    });

  } catch (error) {
    next(error);
  }
});

// Multer error handling
router.use((error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    if (error.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'File too large (max 10MB)' });
    }
    return res.status(400).json({ error: error.message });
  }
  next(error);
});

export default router;

環境設定

// server/config/index.js
import dotenv from 'dotenv';

dotenv.config();

const config = {
  port: parseInt(process.env.PORT || '3001', 10),
  nodeEnv: process.env.NODE_ENV || 'development',

  faculty: {
    password: process.env.FACULTY_PASSWORD
  },

  anthropic: {
    apiKey: process.env.ANTHROPIC_API_KEY
  },

  openai: {
    apiKey: process.env.OPENAI_API_KEY
  },

  cloudinary: {
    cloudName: process.env.CLOUDINARY_CLOUD_NAME,
    apiKey: process.env.CLOUDINARY_API_KEY,
    apiSecret: process.env.CLOUDINARY_API_SECRET
  },

  cors: {
    origin: process.env.CLIENT_URL || 'http://localhost:5173'
  }
};

// Validate required config
const validateConfig = () => {
  const required = [
    'faculty.password',
    'anthropic.apiKey',
    'openai.apiKey'
  ];

  const missing = required.filter(path => {
    const value = path.split('.').reduce((obj, key) => obj?.[key], config);
    return !value;
  });

  if (missing.length > 0) {
    throw new Error(`Missing required config: ${missing.join(', ')}`);
  }
};

validateConfig();

export default config;

Expressルートのテスト

// server/routes/generate.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import express from 'express';
import generateRoutes from './generate.js';

// Mock the claude service
vi.mock('../services/claude.js', () => ({
  generatePage: vi.fn()
}));

import { generatePage } from '../services/claude.js';

const app = express();
app.use(express.json());
app.use('/api/generate', generateRoutes);

describe('Generate Routes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should generate page successfully', async () => {
    generatePage.mockResolvedValue({
      message: 'Generated successfully',
      html: '<html>...</html>'
    });

    const response = await request(app)
      .post('/api/generate')
      .send({
        config: { topic: 'React', depthLevel: 2 },
        message: 'Create a page',
        history: []
      });

    expect(response.status).toBe(200);
    expect(response.body.html).toBe('<html>...</html>');
  });

  it('should validate required fields', async () => {
    const response = await request(app)
      .post('/api/generate')
      .send({
        config: { depthLevel: 2 }, // Missing topic
        message: 'Test'
      });

    expect(response.status).toBe(400);
    expect(response.body.error).toContain('topic');
  });

  it('should handle errors gracefully', async () => {
    generatePage.mockRejectedValue(new Error('API error'));

    const response = await request(app)
      .post('/api/generate')
      .send({
        config: { topic: 'Test', depthLevel: 2 },
        message: 'Test'
      });

    expect(response.status).toBe(500);
  });
});

チェックリスト

ルート作成前

  • どのHTTPメソッドが適切か?
  • どのような検証が必要か?
  • どのミドルウェアを適用すべきか?
  • どのようなエラーケースを処理する必要があるか?
  • ロジックはサービスレイヤーに配置すべきか?

ルート作成後

  • 入力検証を実装した
  • エラーハンドリングが実装されている
  • 成功レスポンスがよく構造化されている
  • ステータスコードが適切である
  • ビジネスロジックにサービスレイヤーを使用している
  • テストが書かれている
  • ドキュメントを追加した

他のスキルとの連携

  • api-client-patterns: これらのAPIのフロントエンド利用
  • prompt-engineering: Claude APIとの統合
  • react-component-patterns: UIでのAPIレスポンス使用
  • systematic-debugging: APIの問題をデバッグする

避けるべき一般的な過ち

  1. ❌ 入力検証がない
  2. ❌ asyncでtry/catchを使用していない
  3. ❌ ルートハンドラーにビジネスロジックがある
  4. ❌ エラーレスポンスが一貫していない
  5. ❌ CORS設定が不足している
  6. ❌ 設定値がハードコードされている
  7. ❌ リクエストロギングがない
  8. ❌ レート制限がない
  9. ❌ 一般的なタスクにミドルウェアを使用していない
  10. ❌ セキュリティのベストプラクティスを無視している

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
majiayu000
リポジトリ
majiayu000/claude-skill-registry
ライセンス
MIT
最終更新
2026/5/4

Source: https://github.com/majiayu000/claude-skill-registry / ライセンス: MIT

本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: majiayu000 · majiayu000/claude-skill-registry · ライセンス: MIT