Claude
Skills
Sign in
Back

openapi-generator

Included with Lifetime
$97 forever

Generates OpenAPI 3.0/3.1 specifications from Express, Next.js, Fastify, Hono, or NestJS routes. Creates complete specs with schemas, examples, and documentation that can be imported into Postman, Insomnia, or used with Swagger UI. Use when users request "generate openapi", "create swagger spec", "openapi documentation", or "api specification".

Design

What this skill does


# OpenAPI Generator

Generate OpenAPI 3.0/3.1 specifications from your API codebase automatically.

## Core Workflow

1. **Scan routes**: Find all API route definitions
2. **Extract schemas**: Types, request/response bodies, params
3. **Build paths**: Convert routes to OpenAPI path objects
4. **Generate schemas**: Create component schemas from types
5. **Add documentation**: Descriptions, examples, tags
6. **Export spec**: YAML or JSON format

## OpenAPI 3.1 Base Template

```yaml
openapi: 3.1.0
info:
  title: API Title
  version: 1.0.0
  description: API description
  contact:
    email: [email protected]
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: http://localhost:3000/api
    description: Development
  - url: https://api.example.com
    description: Production

tags:
  - name: Users
    description: User management endpoints
  - name: Products
    description: Product catalog endpoints

paths: {}

components:
  schemas: {}
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key

security:
  - bearerAuth: []
```

## TypeScript to OpenAPI Schema Converter

```typescript
// scripts/type-to-schema.ts
import * as ts from "typescript";

interface OpenAPISchema {
  type?: string;
  properties?: Record<string, OpenAPISchema>;
  required?: string[];
  items?: OpenAPISchema;
  $ref?: string;
  enum?: string[];
  format?: string;
  description?: string;
  example?: unknown;
}

function typeToOpenAPISchema(
  checker: ts.TypeChecker,
  type: ts.Type
): OpenAPISchema {
  // Handle primitives
  if (type.flags & ts.TypeFlags.String) {
    return { type: "string" };
  }
  if (type.flags & ts.TypeFlags.Number) {
    return { type: "number" };
  }
  if (type.flags & ts.TypeFlags.Boolean) {
    return { type: "boolean" };
  }

  // Handle arrays
  if (checker.isArrayType(type)) {
    const elementType = (type as ts.TypeReference).typeArguments?.[0];
    return {
      type: "array",
      items: elementType ? typeToOpenAPISchema(checker, elementType) : {},
    };
  }

  // Handle object types
  if (type.flags & ts.TypeFlags.Object) {
    const properties: Record<string, OpenAPISchema> = {};
    const required: string[] = [];

    type.getProperties().forEach((prop) => {
      const propType = checker.getTypeOfSymbolAtLocation(
        prop,
        prop.valueDeclaration!
      );
      properties[prop.name] = typeToOpenAPISchema(checker, propType);

      // Check if required (no ? modifier)
      if (!(prop.flags & ts.SymbolFlags.Optional)) {
        required.push(prop.name);
      }
    });

    return {
      type: "object",
      properties,
      required: required.length > 0 ? required : undefined,
    };
  }

  // Handle union types (enums)
  if (type.isUnion()) {
    const enumValues = type.types
      .filter((t) => t.isStringLiteral())
      .map((t) => (t as ts.StringLiteralType).value);

    if (enumValues.length > 0) {
      return { type: "string", enum: enumValues };
    }
  }

  return {};
}
```

## Express Route Scanner with JSDoc

```typescript
// scripts/express-openapi.ts
import * as fs from "fs";
import * as path from "path";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";

interface RouteMetadata {
  method: string;
  path: string;
  summary?: string;
  description?: string;
  tags?: string[];
  requestBody?: object;
  responses?: Record<string, object>;
  parameters?: object[];
  security?: object[];
}

function extractJSDocMetadata(comments: string): Partial<RouteMetadata> {
  const metadata: Partial<RouteMetadata> = {};

  // @summary
  const summaryMatch = comments.match(/@summary\s+(.+)/);
  if (summaryMatch) metadata.summary = summaryMatch[1].trim();

  // @description
  const descMatch = comments.match(/@description\s+(.+)/);
  if (descMatch) metadata.description = descMatch[1].trim();

  // @tags
  const tagsMatch = comments.match(/@tags\s+(.+)/);
  if (tagsMatch) metadata.tags = tagsMatch[1].split(",").map((t) => t.trim());

  return metadata;
}

function scanExpressWithOpenAPI(sourceDir: string): RouteMetadata[] {
  const routes: RouteMetadata[] = [];

  // Implementation: traverse files and extract routes with JSDoc comments
  // Similar to postman generator but with OpenAPI-specific metadata

  return routes;
}
```

## OpenAPI Path Generator

```typescript
// scripts/generate-openapi.ts
import * as yaml from "js-yaml";

interface OpenAPISpec {
  openapi: string;
  info: object;
  servers: object[];
  paths: Record<string, object>;
  components: {
    schemas: Record<string, object>;
    securitySchemes?: object;
  };
  tags?: object[];
  security?: object[];
}

function generateOpenAPISpec(
  routes: RouteMetadata[],
  options: {
    title: string;
    version: string;
    description?: string;
    servers: { url: string; description: string }[];
  }
): OpenAPISpec {
  const spec: OpenAPISpec = {
    openapi: "3.1.0",
    info: {
      title: options.title,
      version: options.version,
      description: options.description,
    },
    servers: options.servers,
    paths: {},
    components: {
      schemas: {},
      securitySchemes: {
        bearerAuth: {
          type: "http",
          scheme: "bearer",
          bearerFormat: "JWT",
        },
      },
    },
    tags: [],
  };

  // Collect unique tags
  const tagSet = new Set<string>();

  // Generate paths
  for (const route of routes) {
    const openAPIPath = route.path.replace(/:(\w+)/g, "{$1}");

    if (!spec.paths[openAPIPath]) {
      spec.paths[openAPIPath] = {};
    }

    spec.paths[openAPIPath][route.method.toLowerCase()] = {
      summary: route.summary || `${route.method} ${route.path}`,
      description: route.description,
      tags: route.tags || [extractResourceTag(route.path)],
      parameters: generateParameters(route),
      requestBody: route.requestBody,
      responses: route.responses || generateDefaultResponses(route.method),
      security: route.security,
    };

    // Collect tags
    (route.tags || [extractResourceTag(route.path)]).forEach((t) =>
      tagSet.add(t)
    );
  }

  // Add tags to spec
  spec.tags = Array.from(tagSet).map((name) => ({ name }));

  return spec;
}

function generateParameters(route: RouteMetadata): object[] {
  const params: object[] = [];

  // Extract path parameters
  const pathParamRegex = /:(\w+)/g;
  let match;

  while ((match = pathParamRegex.exec(route.path)) !== null) {
    params.push({
      name: match[1],
      in: "path",
      required: true,
      schema: { type: "string" },
      description: `${match[1]} parameter`,
    });
  }

  return params;
}

function generateDefaultResponses(method: string): object {
  const responses: Record<string, object> = {
    "200": {
      description: "Successful response",
      content: {
        "application/json": {
          schema: { type: "object" },
        },
      },
    },
    "400": {
      description: "Bad request",
      content: {
        "application/json": {
          schema: { $ref: "#/components/schemas/Error" },
        },
      },
    },
    "401": {
      description: "Unauthorized",
    },
    "404": {
      description: "Not found",
    },
    "500": {
      description: "Internal server error",
    },
  };

  if (method === "POST") {
    responses["201"] = {
      description: "Created successfully",
      content: {
        "application/json": {
          schema: { type: "object" },
        },
      },
    };
  }

  if (method === "DELETE") {
    responses["204"] = {
      description: "Deleted successfully",
    };
  }

  return responses;
}

function extractResourceTag(path: string): string {
  const parts = path.split("/").filter(Boolean);
  return parts[0] || "default";
}
```

## Common Schema Components

```yaml
components:
  schemas:
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
Files: 1
Size: 14.4 KB
Complexity: 20/100
Category: Design

Related in Design