mcpField
P
postman-collection-generator

postman-collection-generator

github.com/patricio0312revテスト・品質63687 回インストール41 週間前に更新
リポジトリ

postman-collection-generator について

API開発者向けに、コードベースのルート定義からエンドポイント情報を抽出し、インポート可能なPostman Collection v2.1を生成するスキルです。複数の一般的なフレームワークに対応し、エンドポイントの整理や例、環境変数の追加も行います。

主な機能

  1. 01APIルート定義のスキャン
  2. 02メソッド、パス、リクエスト情報の抽出
  3. 03リソースやフォルダーごとの整理
  4. 04Postman Collection v2.1 JSONの生成
  5. 05リクエスト例と環境テンプレートの追加

活用シーン

  1. 01既存コードベースからPostmanコレクションを生成
  2. 02リソース別にAPIエンドポイントを整理
  3. 03開発用のベースURLと認証変数を設定
  4. 04ルート変更後にコレクションを再生成

インストール

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

$npx skills add https://github.com/patricio0312rev/skills --skill postman-collection-generator

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

詳細

Postman コレクション生成ツール

API のコードベースから、インポート可能な Postman コレクションを自動生成します。

基本的なワークフロー

  1. ルートをスキャン:コードベース内のすべての API ルート定義を検索します
  2. メタデータを抽出:メソッド、パス、パラメーター、リクエストボディ、ヘッダーを抽出します
  3. エンドポイントを整理:リソースまたはフォルダー構成ごとにグループ化します
  4. コレクションを生成:Postman Collection v2.1 の JSON を作成します
  5. サンプルを追加:リクエストとレスポンスのサンプルを含めます
  6. 変数を設定:ベース URL や認証トークンの環境変数を設定します

対応フレームワーク

フレームワークルートパターン検出方法
Expressapp.get(), router.post()app/router に対するメソッドチェーン
Next.jsapp/api/**/route.tsファイルベースのルーティング
Fastifyfastify.get(), route schemaメソッドとスキーマデコレーター
Honoapp.get(), app.post()Express と同様
NestJS@Get(), @Post() decoratorsデコレーターベース
Koarouter.get(), router.post()Koa-router のパターン

Postman Collection v2.1 スキーマ

{
  "info": {
    "name": "API Collection",
    "description": "Auto-generated from codebase",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [],
  "variable": [],
  "auth": {}
}

Express ルートスキャナー

// scripts/generate-postman.ts
import * as fs from "fs";
import * as path from "path";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";

interface RouteInfo {
  method: string;
  path: string;
  name: string;
  description?: string;
  params?: ParamInfo[];
  body?: Record<string, unknown>;
  headers?: Record<string, string>;
}

interface ParamInfo {
  name: string;
  type: "path" | "query";
  description?: string;
  example?: string;
}

function scanExpressRoutes(filePath: string): RouteInfo[] {
  const routes: RouteInfo[] = [];
  const code = fs.readFileSync(filePath, "utf-8");

  const ast = parse(code, {
    sourceType: "module",
    plugins: ["typescript"],
  });

  traverse(ast, {
    CallExpression(nodePath) {
      const callee = nodePath.node.callee;

      if (callee.type === "MemberExpression") {
        const method = callee.property.name;
        const httpMethods = ["get", "post", "put", "patch", "delete"];

        if (httpMethods.includes(method)) {
          const args = nodePath.node.arguments;
          if (args[0]?.type === "StringLiteral") {
            const routePath = args[0].value;

            routes.push({
              method: method.toUpperCase(),
              path: routePath,
              name: generateRouteName(method, routePath),
              params: extractParams(routePath),
            });
          }
        }
      }
    },
  });

  return routes;
}

function extractParams(routePath: string): ParamInfo[] {
  const params: ParamInfo[] = [];
  const pathParamRegex = /:(\w+)/g;
  let match;

  while ((match = pathParamRegex.exec(routePath)) !== null) {
    params.push({
      name: match[1],
      type: "path",
      example: `{{${match[1]}}}`,
    });
  }

  return params;
}

function generateRouteName(method: string, path: string): string {
  const cleanPath = path.replace(/[/:]/g, " ").trim();
  return `${method.toUpperCase()} ${cleanPath}`;
}

Next.js App Router スキャナー

// scripts/scan-nextjs-routes.ts
import * as fs from "fs";
import * as path from "path";
import { glob } from "glob";

interface NextApiRoute {
  method: string;
  path: string;
  filePath: string;
}

async function scanNextJsRoutes(appDir: string): Promise<NextApiRoute[]> {
  const routes: NextApiRoute[] = [];
  const routeFiles = await glob(`${appDir}/**/route.{ts,js}`);

  for (const file of routeFiles) {
    const content = fs.readFileSync(file, "utf-8");
    const relativePath = path.relative(appDir, path.dirname(file));
    const apiPath = "/" + relativePath.replace(/\\/g, "/");

    // Detect exported HTTP methods
    const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];

    for (const method of methods) {
      if (
        content.includes(`export async function ${method}`) ||
        content.includes(`export function ${method}`) ||
        content.includes(`export const ${method}`)
      ) {
        routes.push({
          method,
          path: convertNextPathToPostman(apiPath),
          filePath: file,
        });
      }
    }
  }

  return routes;
}

function convertNextPathToPostman(nextPath: string): string {
  // Convert [param] to :param
  return nextPath
    .replace(/\[\.\.\.(\w+)\]/g, ":$1*") // [...slug] -> :slug*
    .replace(/\[(\w+)\]/g, ":$1"); // [id] -> :id
}

Fastify ルートスキャナー

// scripts/scan-fastify-routes.ts
interface FastifyRoute {
  method: string;
  path: string;
  schema?: {
    body?: object;
    querystring?: object;
    params?: object;
    response?: object;
  };
}

function scanFastifyRoutes(filePath: string): FastifyRoute[] {
  const routes: FastifyRoute[] = [];
  const code = fs.readFileSync(filePath, "utf-8");

  // Match fastify.get('/path', { schema: ... }, handler)
  const routeRegex =
    /fastify\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*(\{[\s\S]*?\})\s*,/g;

  let match;
  while ((match = routeRegex.exec(code)) !== null) {
    const [, method, path, optionsStr] = match;

    routes.push({
      method: method.toUpperCase(),
      path,
      // Parse schema from options if available
    });
  }

  return routes;
}

コレクションジェネレーター

// scripts/generate-collection.ts
interface PostmanCollection {
  info: {
    name: string;
    description: string;
    schema: string;
  };
  item: PostmanItem[];
  variable: PostmanVariable[];
  auth?: PostmanAuth;
}

interface PostmanItem {
  name: string;
  request: {
    method: string;
    header: PostmanHeader[];
    url: PostmanUrl;
    body?: PostmanBody;
    description?: string;
  };
  response?: PostmanResponse[];
}

interface PostmanUrl {
  raw: string;
  host: string[];
  path: string[];
  query?: PostmanQuery[];
  variable?: PostmanPathVariable[];
}

interface PostmanVariable {
  key: string;
  value: string;
  type: string;
}

function generatePostmanCollection(
  routes: RouteInfo[],
  options: {
    name: string;
    baseUrl: string;
    description?: string;
    auth?: "bearer" | "basic" | "apikey";
  }
): PostmanCollection {
  const collection: PostmanCollection = {
    info: {
      name: options.name,
      description: options.description || "Auto-generated API collection",
      schema:
        "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    },
    item: [],
    variable: [
      { key: "baseUrl", value: options.baseUrl, type: "string" },
      { key: "authToken", value: "", type: "string" },
    ],
  };

  // Add auth configuration
  if (options.auth === "bearer") {
    collection.auth = {
      type: "bearer",
      bearer: [{ key: "token", value: "{{authToken}}", type: "string" }],
    };
  }

  // Group routes by resource
  const groupedRoutes = groupRoutesByResource(routes);

  for (const [resource, resourceRoutes] of Object.entries(groupedRoutes)) {
    const folder: PostmanItem = {
      name: resource,
      item: resourceRoutes.map((route) => createPostmanRequest(route)),
    };
    collection.item.push(folder);
  }

  return collection;
}

function createPostmanRequest(route: RouteInfo): PostmanItem {
  const pathSegments = route.path.split("/").filter(Boolean);

  const item: PostmanItem = {
    name: route.name,
    request: {
      method: route.method,
      header: [
        { key: "Content-Type", value: "application/json", type: "text" },
      ],
      url: {
        raw: `{{baseUrl}}${route.path}`,
        host: ["{{baseUrl}}"],
        path: pathSegments,
        variable: route.params
          ?.filter((p) => p.type === "path")
          .map((p) => ({
            key: p.name,
            value: p.example || "",
            description: p.description,
          })),
      },
      description: route.description,
    },
  };

  // Add request body for POST/PUT/PATCH
  if (["POST", "PUT", "PATCH"].includes(route.method) && route.body) {
    item.request.body = {
      mode: "raw",
      raw: JSON.stringify(route.body, null, 2),
      options: { raw: { language: "json" } },
    };
  }

  return item;
}

function groupRoutesByResource(
  routes: RouteInfo[]
): Record<string, RouteInfo[]> {
  const groups: Record<string, RouteInfo[]> = {};

  for (const route of routes) {
    // Extract resource from path (e.g., /api/users/:id -> users)
    const parts = route.path.split("/").filter(Boolean);
    const resource = parts[1] || parts[0] || "root";

    if (!groups[resource]) {
      groups[resource] = [];
    }
    groups[resource].push(route);
  }

  return groups;
}

CLI スクリプト

#!/usr/bin/env node
// scripts/postman-gen.ts
import * as fs from "fs";
import * as path from "path";
import { program } from "commander";

program
  .name("postman-gen")
  .description("Generate Postman collection from API routes")
  .option("-f, --framework <type>", "Framework type", "express")
  .option("-s, --source <path>", "Source directory", "./src")
  .option("-o, --output <path>", "Output file", "./postman-collection.json")
  .option("-n, --name <name>", "Collection name", "API Collection")
  .option("-b, --base-url <url>", "Base URL", "http://localhost:3000")
  .option("-a, --auth <type>", "Auth type (bearer|basic|apikey)")
  .parse();

const options = program.opts();

async function main() {
  let routes: RouteInfo[] = [];

  switch (options.framework) {
    case "express":
      routes = await scanExpressProject(options.source);
      break;
    case "nextjs":
      routes = await scanNextJsRoutes(path.join(options.source, "app/api"));
      break;
    case "fastify":
      routes = await scanFastifyProject(options.source);
      break;
    default:
      console.error(`Unsupported framework: ${options.framework}`);
      process.exit(1);
  }

  const collection = generatePostmanCollection(routes, {
    name: options.name,
    baseUrl: options.baseUrl,
    auth: options.auth,
  });

  fs.writeFileSync(options.output, JSON.stringify(collection, null, 2));
  console.log(`Generated ${options.output} with ${routes.length} endpoints`);
}

main();

環境テンプレート

{
  "name": "Development",
  "values": [
    { "key": "baseUrl", "value": "http://localhost:3000/api", "enabled": true },
    { "key": "authToken", "value": "", "enabled": true, "type": "secret" },
    { "key": "userId", "value": "1", "enabled": true }
  ]
}

出力例

{
  "info": {
    "name": "My API",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Users",
      "item": [
        {
          "name": "GET users",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/users",
              "host": ["{{baseUrl}}"],
              "path": ["users"],
              "query": [
                { "key": "page", "value": "1" },
                { "key": "limit", "value": "10" }
              ]
            }
          }
        },
        {
          "name": "GET user by ID",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/users/:id",
              "host": ["{{baseUrl}}"],
              "path": ["users", ":id"],
              "variable": [{ "key": "id", "value": "{{userId}}" }]
            }
          }
        },
        {
          "name": "POST create user",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"John Doe\",\n  \"email\": \"[email protected]\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/users",
              "host": ["{{baseUrl}}"],
              "path": ["users"]
            }
          }
        }
      ]
    }
  ],
  "variable": [
    { "key": "baseUrl", "value": "http://localhost:3000/api" },
    { "key": "authToken", "value": "" }
  ]
}

ベストプラクティス

  1. 変数を使用する:柔軟に設定できるように {{baseUrl}}、{{authToken}} を使用します
  2. エンドポイントをグループ化する:リソース/機能ごとのフォルダーに整理します
  3. 説明を追加する:各エンドポイントの目的を記載します
  4. サンプルを含める:現実的なデータをリクエストボディにあらかじめ入力します
  5. 認証を設定する:コレクションレベルの認証を設定します
  6. テストを追加する:基本的なレスポンス検証スクリプトを含めます
  7. バージョン管理する:コレクションの JSON をリポジトリにコミットします
  8. CI と統合する:ルートの変更時に自動生成します

出力チェックリスト

  • コードベースからすべてのルートをスキャン済み
  • エンドポイントをリソースごとにグループ化済み
  • パスパラメーターを抽出済み
  • POST/PUT/PATCH 用のリクエストボディを含め済み
  • 環境変数を設定済み
  • 認証を設定済み(該当する場合)
  • コレクションを v2.1 JSON としてエクスポート済み
  • 環境テンプレートを作成済み

コメント