Loading...
Loading...
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".
npx skill4agent add patricio0312rev/skills openapi-generatoropenapi: 3.1.0
info:
title: API Title
version: 1.0.0
description: API description
contact:
email: api@example.com
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: []// 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 {};
}// 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;
}// 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";
}components:
schemas:
Error:
type: object
required:
- code
- message
properties:
code:
type: string
example: "VALIDATION_ERROR"
message:
type: string
example: "Invalid request data"
details:
type: object
additionalProperties:
type: array
items:
type: string
Pagination:
type: object
properties:
page:
type: integer
minimum: 1
example: 1
limit:
type: integer
minimum: 1
maximum: 100
example: 10
total:
type: integer
example: 156
total_pages:
type: integer
example: 16
PaginatedResponse:
type: object
properties:
success:
type: boolean
example: true
data:
type: array
items: {}
meta:
$ref: "#/components/schemas/Pagination"
User:
type: object
required:
- id
- email
- name
properties:
id:
type: string
format: uuid
example: "123e4567-e89b-12d3-a456-426614174000"
email:
type: string
format: email
example: "user@example.com"
name:
type: string
example: "John Doe"
created_at:
type: string
format: date-time
example: "2024-01-15T10:30:00Z"
CreateUserRequest:
type: object
required:
- email
- name
- password
properties:
email:
type: string
format: email
name:
type: string
minLength: 2
maxLength: 100
password:
type: string
format: password
minLength: 8// Fastify with @fastify/swagger
import Fastify from "fastify";
import swagger from "@fastify/swagger";
import swaggerUi from "@fastify/swagger-ui";
const fastify = Fastify({ logger: true });
await fastify.register(swagger, {
openapi: {
info: {
title: "My API",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
},
});
await fastify.register(swaggerUi, {
routePrefix: "/docs",
});
// Routes with schema
fastify.get(
"/users/:id",
{
schema: {
params: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
},
required: ["id"],
},
response: {
200: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
email: { type: "string" },
},
},
},
},
},
async (request, reply) => {
// Handler
}
);// NestJS with @nestjs/swagger
import { Controller, Get, Post, Body, Param } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from "@nestjs/swagger";
@ApiTags("users")
@Controller("users")
export class UsersController {
@Get()
@ApiOperation({ summary: "Get all users" })
@ApiResponse({ status: 200, description: "List of users", type: [UserDto] })
findAll() {
// Implementation
}
@Get(":id")
@ApiOperation({ summary: "Get user by ID" })
@ApiResponse({ status: 200, description: "User found", type: UserDto })
@ApiResponse({ status: 404, description: "User not found" })
findOne(@Param("id") id: string) {
// Implementation
}
@Post()
@ApiOperation({ summary: "Create new user" })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, description: "User created", type: UserDto })
create(@Body() createUserDto: CreateUserDto) {
// Implementation
}
}#!/usr/bin/env node
// scripts/openapi-gen.ts
import * as fs from "fs";
import * as yaml from "js-yaml";
import { program } from "commander";
program
.name("openapi-gen")
.description("Generate OpenAPI specification from API routes")
.option("-f, --framework <type>", "Framework (express|nextjs|fastify)", "express")
.option("-s, --source <path>", "Source directory", "./src")
.option("-o, --output <path>", "Output file", "./openapi.yaml")
.option("-t, --title <name>", "API title", "My API")
.option("-v, --version <version>", "API version", "1.0.0")
.option("--json", "Output as JSON instead of YAML")
.parse();
const options = program.opts();
async function main() {
const routes = await scanRoutes(options.framework, options.source);
const spec = generateOpenAPISpec(routes, {
title: options.title,
version: options.version,
servers: [
{ url: "http://localhost:3000/api", description: "Development" },
],
});
const output = options.json
? JSON.stringify(spec, null, 2)
: yaml.dump(spec, { lineWidth: -1 });
fs.writeFileSync(options.output, output);
console.log(`Generated ${options.output} with ${routes.length} endpoints`);
}
main();// scripts/validate-openapi.ts
import SwaggerParser from "@apidevtools/swagger-parser";
async function validateSpec(specPath: string): Promise<void> {
try {
const api = await SwaggerParser.validate(specPath);
console.log(`API name: ${api.info.title}, Version: ${api.info.version}`);
console.log("OpenAPI specification is valid!");
} catch (err) {
console.error("Validation failed:", err.message);
process.exit(1);
}
}