Writing OpenAPI with... OpenAI

With the announcement of ChatGPT Plugins using the OpenAPI spec, developers the world over simultaneously thought: "damn, maybe I should've made those Swagger docs after all, lmao".

For NextJS users (at least), the I wrote a program that automatically introspects the /api folder and finds endpoints within the handler method and uses GPT to infer an OpenAPI spec. Meta!!

How does this work?

First, we define two helper functions: getApiRoutes that uses glob to find all TypeScript files in the pages/api directory, and getHandlerFunction that extracts the handler function from each file using Babel to parse the code and returns an array of {name, content} objects with the name being the filename, and content being a string slice containing only the handler function code.

Then, we define a function called inferOpenApiSpec, which calls both previously defined helper functions to obtain an array of handlers with their respective contents, then loops over each item in this array.
For each handler function found, it sends a message as input to GPT-3 along with some preamble text describing how to convert TS functions into JSDoc YAML.

Once it receives a response from GPT-3 it writes resulting contents into ./openapi/{handlerFunction.name}.js.

/* 
1. get the handler function from files
2. use gpt to infer the openapi yaml from the spec
3. return the result as a string
*/
import { glob } from "glob";
import { readFileWrapper, writeFileWrapper } from "./helpers";
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd());

import { Configuration, OpenAIApi } from "openai";
const configuration = new Configuration({
  organization: "…",
  apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

const SKIP_FILES = []; // files to skip from api folder

import * as babel from "@babel/core";
// needs to be required instead of imported to work
const parser = require("@babel/parser");

const getApiRoutes = async () => {
  const apiRoutes = await glob("pages/api/**/*.ts");
  return apiRoutes;
};

const getHandlerFunction = async (apiRoutes: string[]) => {
  const handlerFunctions = [];
  for (const apiRoute of apiRoutes) {
    const file = await readFileWrapper(apiRoute);

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

    babel.traverse(ast, {
      ExportDefaultDeclaration(path) {
        const declaration = path.node.declaration;
        if (
          declaration.type === "FunctionDeclaration" &&
          declaration.id.name === "handler"
        ) {

          handlerFunctions.push({ content: file.slice(declaration.start, declaration.end), name: apiRoute.slice(10, -3) });
        }
      },
    });
  }

  return handlerFunctions.filter((handlerFunction) => !SKIP_FILES.includes(handlerFunction.name));
};

const inferOpenApiSpec = async () => {
  const apiRoutes = await getApiRoutes();
  const handlerFunctions = await getHandlerFunction(apiRoutes);

  for (const handlerFunction of handlerFunctions) {
    console.log(`Reading ${handlerFunction.name}`);
    const completion = await openai.createChatCompletion({
      model: "gpt-3.5-turbo",
      messages: [
        {
          role: "system",
          content:
            "I am a bot that converts handler functions into OpenAPI specs (the path only). I only return YAML in JSDoc format.",
        },
        {
          role: "user",
          content:
            `I want to convert this handler function with path /api/${handlerFunction.name} into an OpenAPI spec: ${handlerFunction.content}`
        },
      ],
    });
    const res = completion.data.choices[0].message.content;
    const openApiSpec = res.slice(res.indexOf("```yaml") + 7, res.indexOf("```", res.indexOf("```yaml") + 7));
    let extractedJSDoc = openApiSpec.slice(openApiSpec.indexOf("/**") + 3, openApiSpec.indexOf("*/"))
    .replace("openapi", "swagger")
    
    extractedJSDoc = "/**\n" + extractedJSDoc + "\n*/\n";
    console.log("Full response:");
    console.log(res)
    console.log(`Writing ${handlerFunction.name}`);
    await writeFileWrapper(`./openapi/${handlerFunction.name}.js`, extractedJSDoc);
  }
};

inferOpenApiSpec();

bramadams.dev is a reader-supported published Zettelkasten. Both free and paid subscriptions are available. If you want to support my work, the best way is by taking out a paid subscription.