Nov 23, 2022 • 6 min read
Auto-generating OpenAPI documents with TypeScript interfaces
OpenAPI is a wonderful tool to explicitly document your REST API endpoints.
It’s also a particularly verbose YAML-based format that can be difficult to write by hand. Look at this “simple” example from the official docs. It’s almost as if the people who invented OpenAPI expected you to use a dedicated OpenAPI editor tool!
If you use TypeScript in your codebase, you might already have defined types such as request and response bodies for each of your endpoints:
If you want to document your API with OpenAPI, you then need to carefully replicate this information in YAML format. This can be tedious and error-prone. Can we do better?
TypeScript Compiler API
If you use TypeScript, you’re probably most familiar with the tsc tool. However, the typescript npm package offers much more than that. In particular, TypeScript lets you hook into its internals with the TypeScript Compiler API. Unlike the rest of the TypeScript project, this API is officially unstable. In practice, it’s however only seen minor breaking changes (see history). I’ve been using it since 2017, and I’ve only had to tweak my code once. Good job TypeScript team, as always :)
TypeScript Compiler API gives you the following functionality:
- parsing TypeScript source files and reading their AST (abstract syntax tree)
- type-checking TypeScript programs, with detailed access to syntactic and semantic errors
- inspecting the inferred type of any particular node in the AST
- transforming the AST and printing out the modified source code
- doing all kinds of shenanigans with virtual files and so on (even type-checking code entirely in the browser!)
For example, say we have the following TypeScript file:
We can use the following code to extract information about MyType:
This will print the following:
We can go one step further and look at the type of each property:
Here is the output:
This also works with more complex types such as arrays, unions, and so on. For example, when type.isUnion() is true, you can iterate through type.types to check each possible type.
We can go one step further and look at the inferred type of a particular variable. Say we have the following code:
We can check the type of myVariable with:
This will tell us that TypeScript inferred its type as the number literal 123:
This means that we can take any TypeScript code and introspect not only types, but also variables in the codebase.
From TypeScript to OpenAPI
Using this knowledge, it’s possible to transform TypeScript into JSON Schema, and by extension OpenAPI (which is 100% compatible with JSON Schema since v3.1).
We’re not going to implement this here because it would take a few thousand lines of code. Instead, let’s walk through how the open-source project tsoa makes it all work.
First, let’s get familiar with the syntax that tsoa uses and walk through the following example from the documentation:
How does tsoa transform this into an OpenAPI document?
It all starts with the ControllerGenerator class, which takes a controller definition and uses TypeScript to inspect arguments passed to each decorator. This extracts useful metadata such as each endpoint’s path, its HTTP method, query parameters, and so on. Each method (getUser and createUser here) gets processed by the MethodGenerator class. Each method parameter (such as userId, name and requestBody) is in turn processed by the ParameterGenerator class.
The real magic happens in the TypeResolver class, where the type of each parameter is resolved and converted to tsoa's internal type model. This is where the TypeScript Compiler API really comes into play. Looking at the code, you can see that covering every possible type isn’t easy. That’s already a thousand lines of code, albeit very well-structured and readable.
Once the code has been processed through the ControllerGenerator using the TypeResolver, tsoa has all the metadata it needs to output the corresponding OpenAPI document. Now, it’s just a matter of converting the metadata to the right format and output it as YAML. This happens in the generateSpec() method, which in the case of OpenAPI 3 will invoke the SpecGenerator3 class. All that class does is convert from tsoa's internal metadata format to the official OpenAPI 3 specification.
The cherry on top: automatic request validation
The beauty of a tool such as tsoa is that it provides not only OpenAPI document generation, but also the ability to automatically validate incoming requests (see documentation). If a client attempts to send a payload field with a number when it should be a string for example, an error will be thrown before the endpoint is executed. We don’t need to write any validation code ourselves; that comes out of the box thanks to the types inferred by tsoa with the TypeScript Compiler API.
While an invalid request could come from a malicious hacker trying to break your API, it could also very well be an error in your client code. This is where Highlight comes in. When an error is thrown, not only can you see the server-side error stack in Highlight, but you’ll be able to replay the session that led to this bug in the first place from the user’s perspective. Knowing what circumstances led to a particular bug can be essential to fix it!
Get the visibility you need
What will you use the TypeScript Compiler API for?
Generating OpenAPI documents is but one possible application of the TypeScript Compiler API. You could also use it to generate forms from types automatically, to auto-generate React component properties, and so on.
What will you use the TypeScript Compiler API for? Let me know and don’t hesitate to ask for help if you get stuck!