loading...

Generating OpenAPI clients for FastAPI via Gradle

mxab profile image Max Bruchmann ・3 min read

FastAPI is a neat python server framework that allows us to setup a server quickly . It automatically generates an OpenAPI spec via its decorated methods and exposes it when the server is running.

Sometimes we want to generate some kind of client directly e.g. for an Angular application in the same repo or for some other client maybe for some E2E Tests without launching the server.

This post demonstrates how we can generate a client for Angular via Gradle and the OpenAPITools generators.

This approach can be used to generate any kind of client that is supported by OpenAPITools.

The full working demo can be found here:

https://github.com/mxab/fastapi-angular-openapi-gradle-demo

Let's assume our repo structure looks like this:

.
├── fastapi-server
│   ├── build.gradle
│   ├── fastapi_server
..
│   ├── poetry.lock
│   ├── pyproject.toml
└── ng-app
│   ├── build.gradle
│   ├── angular.json
..
    └── src
        ├── app

Exporting the OpenAPI Spec

The fastapi-server directory contains the server code and organises its dependencies via poetry

To now export an OpenAPI JSON file we create a Gradle task inside this project that looks like this:

Inside a FastAPI application you can programmatically access the OpenAPI Spec as a dict via app.openapi(). Let's leverage this function to generate the json spec:

ext {
// the location of the exported file
    openapiJSONSpec = file("$buildDir/openapi/fastapi-server.spec.json")
}

tasks.register("exportOpenAPISpec", Exec) {
    commandLine = [
        "poetry",
        "run",
        "python", 
        "-c",
        """
        import json
        import sys
        from fastapi_server.app import app
        json.dump(app.openapi(),sys.stdout)
        """.stripIndent()]

    openapiJSONSpec.parentFile.mkdirs()//otherwise FileOutputStream throws
    standardOutput = new FileOutputStream(openapiJSONSpec)

    inputs.dir file("fastapi_server")
    outputs.file openapiJSONSpec
}

This Exec Tasks launches a python command via poetry so we automatically use the correct venv.

It imports the FastAPI app retrieves the openapi dict and dumps it as json to the stdout.

The Gradle task prints the output of the process to a file.

You can run ./gradlew fastapi-server:exportOpenAPISpec to see the result.

But we are not completely done inside this project. That we can consume this generate json artifact from other projects inside our multi project we need to make Gradle aware of it.

Therefore we create a new configuration called openapi that can be consumed.

configurations {
    openapi {
        canBeConsumed = true
        canBeResolved = false
    }
}

And we also tell Gradle that the generated file is an artifact of this configuration that is build by the task we registered:

artifacts {
    openapi(openapiJSONSpec){
        builtBy(exportOpenAPISpec)
    }
}

Check the Gradle docs for more details on this.

Generate the angular client

In your angular project we want to use the OpenAPITools generator to generate a client.

OpenAPITools Gradle plugin

We can use the official Gradle plugin for that.

plugins {
    id "org.openapi.generator" version "4.3.0"
}

Retrieving the spec file as dependency

In this project we also define a configuration that is resolvable and add the fastapi-server consumable config as dependency.

configurations {
  openapi {
    canBeConsumed = false
    canBeResolved = true
  }
}
dependencies {
  openapi(project(path: ":fastapi-server", configuration: 'openapi'))
}

Dependency as input for the generator task

We can now simply tell the openApiGenerate task that it should use the resolved file from the openapi configuration.

ext {
    generator = "typescript-angular"
    clientOutputDir = file("$projectDir/src/client")
}
openApiGenerate {
    generatorName = generator
    outputDir = clientOutputDir.path
    inputSpec = configurations.openapi.singleFile.path
}

//unfortunatly the actual task does not know anything about inputs and outputs. This tweak fixes it
tasks.named("openApiGenerate"){
    inputs.files configurations.openapi
    outputs.dir clientOutputDir
}

When we now run

./gradlew ng-app:openApiGenerate

it first runs the export task and then the generate task. When we look into ng-app/src/client we will see a ready made client for our backend to use.

Use in angular

In our main app.module.ts we can now import it as a module:

import { ApiModule, BASE_PATH } from '../client';
...
import { environment } from './../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
   ...
    ApiModule
  ],
  providers: [{
    provide: BASE_PATH, useValue: environment.apiUrl
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }

And finally we inject the DefaultService where it's needed:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
 constructor(private readonly defaultService: DefaultService){
 ...
  this.defaultService.listUsersUsersGet()
 ...
 }
}

Conclusion

Gradle allows us to enhance our project setup easily, especially when there are multiple heterogeneous projects.

The export part could be adjusted to whatever python tooling that is used (conda, pipenv...)

The the OpenAPITools project provides many different generators which allows us to adjust this show case for different use cases.

Posted on by:

Discussion

markdown guide