Generating Dart REST API client libraries using OpenAPI Generator
REST APIs are ubiquitous in modern software development, and many applications will need to integrate them in order to connect to external services.
Manually building client SDKs for such APIs usually involves writing a lot of boilerplate code and making sure the code adheres to the API’s specifications.
OpenAPI (formerly known as Swagger) is an open-source specification for describing REST APIs in a language-agnostic way that allows tools to generate SDKs for consuming those APIs.
Creating an OpenAPI (or Swagger) spec file is outside the scope of this post, but we’re going to take a look at how to generate a Dart SDK that we can consume in a Flutter app from such a file, using OpenAPI Generator.
Setup
First, let’s create a local package to hold our SDK client (optional, but encouraged in order to keep things separate and modular).
Normally we’d do this by running
flutter create --template=package my_api
but since our generator will create a pubspec.yaml
file and a lib folder for us, this would just add unnecessary files, so simply creating an empty folder is actually better in this case.
Next, gather the necessary ingredients and place them in the package’s root:
- The OpenAPI spec’s
.yaml
file. You can use the sample Petstore spec file for test purposes if you don’t have one of your own. - The OpenAPI Generator which will turn our spec file into an API client.
There are several forms this generator can take, including online generators, but for now let’s just grab the latest JAR from the OpenAPITools Maven repository.
Ensure your Java runtime is accessible from the command line and runjava -jar openapi-generator-cli.jar help
to validate that everything is OK.
Our package folder should look like this at this point. A very simple garden, with only a seed (the spec file) and some fertilizer (the generator) to make it grow.
Generating the API client
Generator selection
The OpenAPI Generator supports generating clients in over 60 language/library combinations, which you can list by running java -jar openapi-generator-cli.jar list
. For our purposes, we’re only interested in the Dart generators, of which there are three at the time of writing:
dart
- Uses the
http
package as its HTTP client, and this is the only direct dependency of the generated client. Model classes are generated as Plain Old Dart Objects (PODOs) withtoJson
andfromJson
methods.
dart-dio
- Uses the
dio
package as its HTTP client, andbuilt_value
to generate models and their serializers.
dart-jaguar
- Uses the
jaguar-retrofit
andjaguar-serializer
pair as the HTTP client and serializer, respectively.
We’ll look at dart-dio
specifically since that’s what I’m most familiar with, but the other generators should work similarly.
Generator configuration
First, let’s look at the configuration options for this generator. We can define these settings in an open-generator-config.yaml
file that we can pass to the generator, for example:
# Config options for the dart-dio generator
pubName: my_api
pubVersion: 0.0.1
pubDescription: "My API Client"
If you want the generator to automatically format your code, you’ll need to set a DART_POST_PROCESS_FILE environment variable pointing to dartfmt -w
in your Dart SDK’s path. Some generators will inform you about this if the variable is not set.
The help command will document all the options for the generation command, but at a minimum, we’ll need to feed it the spec file as input (-i
) and specify a generator (-g
).
We’re additionally passing the generator configuration file (-c
) and enabling the automatic code formatting (--enable-post-process-file
).
If you want to not generate test classes, there’s -DapiTests=false
and -DmodelTests=false
, as well as -DapiDocs=false
for documentation.
There’s a slew of other options that you can explore, like selectively generating only certain parts of the API, bringing in your own pre-generated models, or ignoring generating certain files (the generator should create an .openapi-generator-ignore
file where you can add exclusions).
Running the generator
Finally, let’s generate the client:
java -jar openapi-generator-cli.jar generate -i petstore.yaml -g dart-dio -c open-generator-config.yaml --enable-post-process-file
Et voilà, a fresh API client package.
Or so I’d like to say, but in the case of this particular generator, we’re going to have to run
flutter pub run build_runner build
to generate our built_value
models and complete the process for good.
As a small troubleshooting tip on this step, if any of your generated model classes is named the same as a class in dart:core
(e.g. Error
or Match
) you will probably run into the problem described in this GitHub issue, which you can solve by hiding the offending classes in the generated serializers.dart
file:
import 'dart:core' hide Error;
and then running the flutter pub run build_runner build
command again.
Sadly, if this is the case, which it will be if you used the sample spec file linked above, you’re probably going to have to do this every time you regenerate the client, at least until you’re comfortable enough with the toolset to write your own codegen templates, which use the Mustache templating system.
The preceding paragraph marks this as a good point for a reality check: code generation is not perfect, especially not when it tries to cast such a wide net as OpenAPI Generator. You are likely to run into errors and things might not work out-of-the-box without some trial-and-error.
Take this exercise as a starting point to offload some of the boilerplate and validate your API spec. If you have the ability to change your spec, customize an existing generator’s options or even write your own generator to the point it all works flawlessly with just a couple commands, you’re one step closer to the code equivalent of Midas’ Golden Touch.
Using the generated API client
Generator output overview
If all went well, there should be a new lib folder in our package, along with doc and test if those were generated as well.
Inside the lib folder, there should be an api folder, with a file for each endpoint defined in our API, wherein their operations are called, parameters parsed, and responses deserialized.
The model folder contains, as the name implies, our built_value
models and their generated parts.
At the package root level, there’s the aforementioned serializers.dart
and the main entry point to our package, api.dart
.
Notice that this class creates a default Dio client if none is provided. You’ll probably want to feed your own Dio instance to the API so you can add things like interceptors, so let’s do that in our app package (after adding the newly created package to the pubspec, of course):
Now we can create an instance of our API client and access it.
Conclusion
Maintaining an OpenAPI spec can definitely assist API development by being a single source of truth, but in my opinion, that might not true if all you use it for is generating documentation.
If both the client and the server depend on the spec definition for generating their code, or if we generate the spec from the server code, then the client SDK from the spec, we can ensure that the code actually matches the documentation. This consistency can greatly accelerate iteration times when developing new features, since you don’t have to keep worrying about what headers that new endpoint requires, how the response is formatted, or crafting HTTP requests / transient data classes by hand. This allows us to keep our heads in the actual application code.
The caveat, like most automation solutions, is the initial friction in setting it up and dealing with occasional annoyances and necessary workarounds. A code generator is another piece of software that can run into bugs and unexpected behavior, and has to be understood and accepted, benefits and limitations alike, by those who use it.