Skip to content

OpenAPI documentation and string templating

One of the ways _not_ to write an OpenAPI documentation

Posted on:
3 min

Just use Redoc

I believe good documentation helps business. Moreover, it is cheap to maintain documentation when it is automatically generated from code or comments. At most of the small startups I have worked with, Redoc is used to generate documentation using an OpenAPI spec. However, I don’t like Redoc for the following reasons:

  1. Using the free version means living with the Redoc branding
  2. The free version is essentially a huge React app that parses the OpenAPI spec, resolves the references, transforms the parsed spec and then renders the documentation. This takes a lot of time and causes the page to crash if you have a relatively large public API.
  3. It is quite ugly (in my opinion) and customization is not allowed in the free version.
  4. It prevents co-location with product documentation, which is generally hosted on applications like mkDocs, or Gitbook.

A picture of a Redoc generated website for the generic petstore API

When I started looking for alternatives to generate Web API documentation, I evaluated Slate, Docusaurus OpenAPI Docs, Starlight, Nextra, Readme, and Mintlify which is a Nextra fork, I guess.

At the time of evaluation, Slate was not customizable enough, Docusaurus OpenAPI Docs was full of bugs, Starlight did not support co-location, Nextra had no support for generating OpenAPI documentation, and Mintlify was just starting to support OpenAPI docs. Of the evaluated solutions, only Readme met all functional and non-functional requirements I had drafted, but it was too expensive.

It was clear that we could not use a solution that met our budget and requirements out of the box. I decided to spend some time trying to build something on top of Nextra.

An incomplete sitemap for the website

A top-level sitemap for the documentation

I started with hacking into our swagger.json using @apidevtools/swagger-parser. Later, I realized that I could have used @readme/openapi-parser for the added bonus of having more detailed error messages. Using @apidevtools/swagger-parser, I could easily generate tree-like structures that I would then use to generate a configuration for the navigation sidebar (_meta.json for Nextra, sidebars.js for Docusaurus), and naively, I thought I could also build the pages with ease.

I found that the documentation Redoc was generating depended on some non-standard properties specific to Redoc, like ‘x-tagGroups’. This prevented me from building a sidebar navigation that would be equivalent to the one Redoc was creating without having to write a lot of code and type overrides.

import {
  AppStore,
  GroupModel,
  IMenuItem,
  OperationModel,
  isAbsoluteUrl,
  loadAndBundleSpec,
} from "redoc";

This could have been a possible opportunity to backtrack and think of generating a swagger.json that would work without the non-standard Redoc properties, however, I decided to look into the Redoc source code and found that they exported a majority of the functions I would need to create a navigation bar equivalent to Redoc.

function createFilesForItems(menuItems: IMenuItem[], outputDir: string, parentMetaJson: MetaJson) {
  for (const item of menuItems) {
    if (item.type === 'operation') {
      // Coerce type as per Redoc internals
      const operation = item as OperationModel;
      ...
    } else if (item.type === 'section') {
      ...
    } else if (item.type === 'tag') {
      ...
    } else if (item.type === 'group') {
      ...
    } else {
      throw new Error(`Unknown menu item type: ${item.type}`);
    }
  }
}

I wrote a recursive function that would generate directories, MDX files, and the correct _meta.json files based on the documentation structure and I was able to replicate the structure Redoc would create.

if (item.type === "operation") {
  const operation = item as OperationModel;
}

I wasn’t particularly happy about how Redoc does type assertions internally with multiple instances using the as directive where it could just rely on the compiler.

Following this, I was sent down a rabbit hole of components in Redoc that are used to display different combinations and configurations of API endpoints. I’ve uploaded the project to GitHub for reference. I had seen multiple examples of Markdown generation using templating or some sort of an AST so I did not question the approach for long, not until I wrote this.

/**
 * Render Nextra Tabs Component
 * @param items
 * @param children
 * @returns
 */
export function TabsMdx(items: string[], children: (string | null)[]) {
  if (items.length !== children.length) {
    throw new Error("TabsMdx: items and children must be the same length");
  }
  if (items.length === 0 || children.length === 0) {
    return "";
  }

  return `
    import {Tab, Tabs} from 'nextra/components';

    <Tabs items={ ${JSON.stringify(items)} }>
        ${children
          .map(child => {
            if (child) {
              return `<Tab>${child}</Tab>`;
            } else {
              return "";
            }
          })
          .join(`\n`)}
    </Tabs>
    `;
}

I stopped and said to myself, “This is a nightmare for testing”. I can’t possibly test all use cases and the possible outcomes it could lead to, more importantly, unlike with conventionally written code, I cannot make the program fail gracefully.

Next steps

I want to explore the possibility of generating API documentation using a dynamic route approach, similar to using getStaticPaths() in Astro or Next. I tried doing this with Nextra but I was unable to.