OpenAPI documentation and string templating
One of the ways _not_ to write an OpenAPI documentation
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:
- Using the free version means living with the Redoc branding
- 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.
- It is quite ugly (in my opinion) and customization is not allowed in the free version.
- It prevents co-location with product documentation, which is generally hosted on applications like mkDocs, or Gitbook.
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.
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.
- Update (27/11/2023) – I have noticed that Starlight and Mintlify have made significant improvements since I first evaluated it and it might be worth a try.