Type-checking Express APIs

tl;dr:
I write an OpenAPI specification and then generate Express types from it

I was recently asked to provide API documentation for an Express API I wasn't involved in developing.

It might've looked something like this:

My first idea was to generate the API docs automatically from the code. OpenAPI is a popular specification for describing RESTful APIs, that can be consumed by many tools to generate documentation, client code, and server code.

Unfortunately, I wasn't able to find a tool that could generate OpenAPI specs from Express application code. My next idea was to validate the API against a manually written OpenAPI specification.

It would work by starting with a guess at the OpenAPI specification, generating types, and then type-checking the Express application against those types. Then I'd continually refine the specification until there were no more type errors.

Specification-driven development

By generating either the code or the specification, there is only one place where changes need to be made. This ensures the accuracy of the documentation and prevents documentation drift.

The advantage of starting with an OpenAPI specification is that it encourages a language-agnostic, design-first approach to API development.

This simple OpenApi specification, written in YAML, represents a Petstore API with a single endpoint for retrieving a pet by its ID, provided in the url path. The response is a JSON object with either the pet's id and name, or an error message.

With this specification, the API provider can implement it using the language of their choosing and generate API documentation. The consumer can generate client code to interact with the API using something like openapi-generator-cli

Generating TypeScript types

The package openapi-typescript generates TypeScript types from an OpenAPI specification.

Invoking the command above with the sample specification, yields a TypeScript file containing type definitions for each request and response.

Express Types

Let's take a closer look at line 10 of the Express application.

The second argument to app.get is a function with the type RequestHandler

RequestHandler has five generic types parameters.

  1. P: url path parameters
  2. ResBody: the response body
  3. ReqBody: the request body
  4. ReqQuery: query string parameters
  5. LocalData: local data attached to the response

Our goal is to combine our generated OpenAPI types and RequestHandler to produce something like this:

ShowPetByIdHandler is the type of a request handler that can 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, we want to get them from the openapi-typescript generated types.

OpenApiRequestHandler

Given an Operation, we can define a RequestHandler type.

Locals is the type of the local data attached to the response object. Since it's unaffected by the API specification, we pass it through.

Four intermediate types determine the path parameters, response body, request body, and query parameters based on the operation type. Let's look at each of these.

OpenApiPathParams

Path parameters are defined in Operation["parameters"]["path"]

First, we define the utility type Property that extracts the type of a property from an object type.

So, OpenApiPathParams extracts the type of Operation["parameters"]["path"] or undefined if it doesn't exist.

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.

We user the Property utility type in the same way as before to extract request body type from Operation["requestBody"]["content"] . This actually contains a map of MIME types to request body types and the Properties utility type provides a union of the body types or undefined if there are none.

OpenApiResponseBody

Finally, we have the response body type, OpenApiResponseBody, which adds one last level of complexity; multiple response codes with different body types.

The response body is defined for each response code and MIME type in Operation["responses"][responseCode]["content"][mimeType].

Using the tools we've built so far, we can extract and collect responses for each response code before extracting and collection body types for each MIME type.

Type-checking the API

If you didn't follow all that it's ok, this is the important part; updating our app to typecheck against the OpenAPI specification.

Notice that apart from extracting the request handler and specifying it's type, the responses have also changed. That's because the types didn't match the API specification and the type checker caught it.

Type error

Here is the modified showPetByIdHandler type:

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 projects package.json, it's easy to rerun openapi-typescript whenever the specification changes.

As for the original goal of documenting the unfamiliar API; I started with a guess at the specification and refined it iteratively until all of the type errors were gone.