Avoid regression by keeping your server and client code in sync

Sebastian Dragomir
lenses.io
Published in
7 min readJan 20, 2021

In the previous posts Andrea explained why and how to get started with Tapir in order to create OpenAPI definitions from the existing backend codebase. One of the benefits for this, as he explained, was to generate the API docs.

In this part of the series we’ll look at another way to take advantage of the definitions, which is, how to generate type safe Client side code.

The goal is to reduce the chance of regression while at the same time reducing the amount of code that is written on the client. Time to avoid those moments:

Depending on your CI pipeline there are different ways to achieve the generation of the Typescript code.

For the purpose of this exercise we will be using a dedicated repository, but the same applies if you run the commands directly from CLI, using npx for example.

1. Generating source files.

Let’s start by setting up a node repository where the generated OpenAPI spec can be pushed every time it is created. (usually the CI will generate it on merges into the BackEnd repository). The file contains all the definitions in the following format, explained in part 2 of this series.

You can find it located in the openapi folder.

We need to convert this to something that the client understands. As it can be seen, we only need to add a script line in the package.json in order to generate the TypeScript source code. It will call the openapi generator cli, that we added as a dependency to our repo that contains the definitions. Alternatively we can call it directly using npx in one of our CI steps. If we use a repo though, the devs can update the script using PR’s rather than having to touch the CI directly.

generate”: “openapi-generator-cli generate -g typescript-axios -i openapi/backend-api.yml -c config.json -o lib/ui-dto --type-mappings=AnyType=any --additional-properties=npmName=@company/ui-dto,npmRepository=https://myprivatenpmrepo.io/lenses/api/npm/npm/,withSeparateModelsAndApi=true,withInterfaces=true,npmVersion=1.0.0"

Let’s go over some of the options quickly:

  • -g typescript-axios:

Sets the generator type we want to use. In our case, we are interested in the one that generates Axios Services and Typescript Types/Interfaces, but have a look at the other options available to see if any fit your needs better.

  • -i openapi/backend-api.yml

The Yaml file containing the API definitions

  • -c config.json

The configuration that will provide the format/structure of the generated types and services. Adding this will ensure the types and services are split into different folders. It looks like this:

{
“modelPackage”: “types”,
“apiPackage”: “services”
}
  • -o lib/ui-dto

Source output folder.( that will be published next)

  • — type-mappings=AnyType=any

This was added due to an issue found in the build stage (that is presented later in this article) whereby the generator was trying to map `AnyType` to a non existent file ‘any-type.ts’, so we are telling the generator to set any, but you can use unknown if you prefer.

It’s important to note that while there is a decent amount of support for the builtin types as it can be seen here, some of the custom types that you wrote in the BE might not be properly mapped by default so a bit of trial and error is required in the beginning.

As for the additional-properties you can find the full explanation here.

One important thing to note here is the npmVersion. Remember to increment it or at least have it different from the previously generated DTO’s as else the npm publish that we will run later, will fail. Ideally you will want to use the version property that can be found in the yaml openapi file. If you plan on generating multiple times using the same version (for example from branches), then add part of the commit hash to the version, in order to make it unique and easily identifiable. Another advantage to this is that it will ensure that you can track back from any npm version to the Back-end commit that generated them. No more guessing what version of the BE a certain UI commit needs!

2. Building and publishing the code.

Once you’ve run the command you will notice that a file openapitools.json is created. You don’t need to touch that file for now, but if you are interested in how to do so in order to create more customised builds, I recommend the information provided by OpenAPI generator page.

Now that we have the Typescript source generated, let’s build and publish it.

cd lib/ui-dtonpm installnpm audit fix (optional, see explanation below)npm publish

You don’t need to run manually a build command as npm publish will also build the dist folder from the Typescript source which will in turn validate that the generated code is valid. (due to it’s prepublishOnly script)

We added the npm audit fix in order to make sure that the dependency, axios, will be upgraded to the latest version.

You should be able to see the dist folder that was generated and pushed to npm. (make sure that your CI has all the correct tokens set for publishing to your private npm repository)

Folder structure for the generated services.

3. Using the generated types and services in your project

At this point you should be good to start integrating the services and DTOs. In order to use the newly published library, set it as a dependency in your project package.json and run the usual npm/yarn install

"@company/ui-dto": "1.0.0"

Open your node_module folder and look for your newly installed definitions to be sure that all is there.

Let’s use the Acl’s that we highlighted previously as an example. I’m using below a cleaned version of Typescript d.ts file to make it easier to follow.

As it can be seen from the definitions above, we have multiple ways to call the addAcls method ( in a functional or OOP way ). Depending on how your project is setup, choose whichever works best for you.

For this example we’ll choose using the Factory. The reason is that it allows us to inject our custom Axios instance, which in turn allows us for greater customisation of the http calls. (from tokens to retry mechanism, interceptors, etc)

It seems that we tried to call the API without passing on the required arguments. Fortunately Typescript comes to the rescue and we are warned of this at compile time.

Now when the Backend generates a new version and the client is updated to use it, our CI will fail automatically if there are updated APIs and the UI code is not passing the correct arguments anymore. And all of this without writing a single Typescript definition.

Let’s look at the generated ACL interface and fix out code.

Definitely better. Now everything compiles and we avoided the potential miss-matches between the BE and the UI.

Business is happy as there’s fewer bugs and the developers are happy as they now have to write less code due to the code being autogenerated.

Conclusions

In this final part of our Tapir series we showed how using OpenAPI in your project can both decrease the risk of regression and amount of code written on the client side due to the use of automation. The saved time can be used to focus on other business critical tasks.

As with all new tools, it’s best to try it out on a branch before using it in Production, in order to make sure it fits your needs and works with your technology stack. Some workarounds are needed at times, as we saw above with the Any type example, but the project is constantly improved so we expect fewer and fewer issues. Our recommendation is to start your POC with one of your more complex parts of the code, ideally where you have a mix of complex types and enums and see how the generated code looks like, then adjust your OpenApi definitions in order to get stronger types on regeneration.

Another potential disadvantage worth mentioning is that not all of the generators seem to offer the same features, so for example the typescript-axios and typescript-rxjs will have a different implementation and list of supported features due to using different underlying libraries. And while the generators are quite light on dependencies, if you go with any of the two I mentioned here, you will need to make sure that the version it is using is the same as the one you use in your main project. Even though normally this can be fixed, as we did above using npm audit fix, it’s still something you should take into consideration.

We hope this series provided you with a good overview of how OpenAPI & Tapir can help you in your project and if it does, let us know in the comment section.

About us:

Lenses provides a monitoring, self-service administration, governance & secure data access layer on any Kafka & Kubernetes. It allows developers to build, deploy, debug & manage streaming flows in a fraction of the time with reduced risk and more consistency. See our Engineering Blog or Twitter for more articles on Kafka and DataOps.

We are hiring, so be sure to check out our careers page.

--

--

Sebastian Dragomir
lenses.io

Software Engineer. Consultant. Coder. Sometimes bug fixer. Developing Web Apps for Investment Banking, Healthcare, Educational Sector and startups since 2007.