Type-safe Express APIs
I was recently asked to provide API documentation for an unfamiliar Express API. It might've looked something like the ubiquitous pet store example.
OpenAPI is a popular specification for describing RESTful APIs that can be consumed by many tools to generate documentation, client code, and server code.
My first idea was to generate the API docs automatically from the code - an approach I'd used before with Python's FastAPI framework. Unfortunately, I wasn't able to find a tool that could generate OpenAPI specs from Express application code.
My next idea was to start with an OpenAPI specification and use it to type-check the code as well as generate documentation.
Specification-driven development
Regardless of whether you start with the code or the specification, generating one from the other ensures that the two are always aligned and streamlines development through centralized updates.
Specification-driven development (i.e., starting with a specification and generating the code), offers the advantage of being highly standardized and language-agnostic. This makes it easier to get all stakeholders involved earlier in the development process.
This simple OpenAPI specification, written in YAML, represents a pet store API with a single endpoint for retrieving a pet by its ID, which is provided in the URL. The response is a JSON object with either the pet's id and name, or an error code and message.
With this specification, the API provider can implement it using the language of their choosing and generate API documentation. The API consumer can generate client code using tools like openapi-generator-cli.
Generating TypeScript types
The npm package openapi-typescript generates TypeScript types from an OpenAPI specification.
Invoking this command with the sample specification yields a TypeScript file with definitions for each request and response.
Express Types
Let's take a closer look at the Express route definition.
The second argument to app.get is an anonymous function with
the type RequestHandler. Here's the definition provided by Express:
RequestHandler has five generic type parameters.
Our goal is to combine our generated OpenAPI types and RequestHandler to produce something like this:
ShowPetByIdHandler is a type of RequestHandler that receives a single path parameter petId and returns an object
representing a pet, or an error.
OpenAPI types in Express
We don't want to manually specify those generic type parameters though,
they should come from the OpenAPI specification via openapi-typescript, giving us something like this:
OpenApiRequestHandler
Given an Operation, we can define the OpenApiRequestHandler type.
Locals is the type of the local data attached to the response object.
Since it's unaffected by the API specification, it simply gets passed through.
Four intermediate types determine the path parameters, response body,
request body, and query parameters based on the Operation.
Let's look at each of these.
OpenApiPathParams
Path parameters are defined in Operation["parameters"]["path"]. The utility type Property is used to extract the path parameters
type.
So, OpenApiPathParams represents the type of Operation["parameters"]["path"] if it exists, or else undefined.
OpenApiQueryParams
Next, we'll jump to OpenApiQueryParams, since it's nearly the
same. The only differences are the property path and MapToString.
It's possible to specify query parameters as numbers or booleans, but
Express will always parse them as strings. MapToString is a utility
type that converts non-string property types to string types. We ignore array
and object parameters for the sake of simplicity.
OpenApiRequestBody
Now let's look at the third intermediate type, OpenApiRequestBody.
Using the Property utility type as before, we extract the
request body type from Operation["requestBody"]["content"].
This contains a map of MIME types. The Properties utility type provides a union of the body types or undefined
if there are none.
OpenApiResponseBody
The response body type has one more level of complexity: multiple responses with different MIME types.
The response body is defined for each response code and MIME type in Operation["responses"][responseCode]["content"][mimeType].
OpenApiResponseBody extracts and collects response types by response
code and MIME type.
Type-checking the API
If you didn't follow all that it's ok! Here's the important part: typechecking against the OpenAPI specification.
Notice that apart from extracting the request handler and specifying its type, the responses have also changed. That's because the types didn't match the API specification and the type checker caught it.

Testing
Finally, we can use the utility type Equal to verify that the RequestHandler was derived correctly.
Moving Forward
We've seen how to generate TypeScript types from an OpenAPI specification and use them to type-check an Express API. This approach ensures that the API documentation and code remain in sync as the API evolves.
By adding a script to a project's package.json, it's easy to
rerun openapi-typescript whenever the specification changes.
Conclusion
In terms of the original goal of generating API documentation, this approach was quite successful. I started by taking a guess at the specification, or generating one by using online tools and observing requests and responses. The type-checker then guided me to correct the code or the specification until the two were in agreement.
One improvement that would be nice is a way to ensure the response code corresponds with the body type.