Generating apps from OpenAPI specs - Part 2 - Generating application and client code

In this post we will start with generating some code from an OpenAPI spec.

All code for this post can be found here.

The spec we will use is the petstore example taken from OpenAPI's docs.

OpenAPI-Generators

We will use OpenAPI generator for generating code. OpenAPI generator is a collection of generators for many languages. There are other generators, (notably, go-swagger), but OpenAPI generator seems like a good place to start.

In this example we will create a go backend using the go-server generator, and client code using the typescript-fetch generator.

For simplicity, it might be nice if there was a generator for generating a typescript backend, but unfortunately there is not one.

Also, the documentation for these generators, and how to use their generated code, is quite sparse. It might be a task for this writer to submit a PR.

Lets generate some code.

Install openapi-generator following the instructions here.

What we'll do

  1. Use the CLI tool to generate the code boilerplate for the server and client
  2. Identify the entrypoints of the generated code where we can start to modify things, and start wiring in our business logic.

The Spec

Our spec is contained in the top level spec/ folder:

spec
├── petstore-separate
│   ├── common
│   │   └── Error.json
│   └── spec
│       ├── NewPet.json
│       ├── Pet.json
│       ├── parameters.json
│       └── swagger.json

The swagger.json file in the entrypoint for the spec.

Generate the go backend with go-server

The command to run is:

openapi-generator-cli generate -g go-server -o ./backend -i ./spec/petstore-separate/spec/swagger.json 

This creates the following folder structure:

backend
├── .openapi-generator
│   ├── FILES
│   └── VERSION
├── .openapi-generator-ignore
├── Dockerfile
├── README.md
├── api
│   └── openapi.yaml
├── go
│   ├── api.go
│   ├── api_default.go
│   ├── api_default_service.go
│   ├── error.go
│   ├── helpers.go
│   ├── impl.go
│   ├── logger.go
│   ├── model_error.go
│   ├── model_new_pet.go
│   ├── model_new_pet_all_of.go
│   ├── model_pet.go
│   └── routers.go
├── go.mod
└── main.go

The entry point we are interested in is go/api_default_server.go, this is is where we can start wiring in our business logic. Everything else we can leave to be generated from the spec.

Some example code we get is:

// AddPet - 
func (s *DefaultApiService) AddPet(ctx context.Context, pet NewPet) (ImplResponse, error) {
	// TODO - update AddPet with the required logic for this service method.
	// Add api_default_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation.

	//TODO: Uncomment the next line to return response Response(200, Pet{}) or use other options such as http.Ok ...
	//return Response(200, Pet{}), nil

	//TODO: Uncomment the next line to return response Response(0, Error{}) or use other options such as http.Ok ...
	//return Response(0, Error{}), nil

	return Response(http.StatusNotImplemented, nil), errors.New("AddPet method not implemented")
}

So lets wire in some logic. We won't bother with a database for the purposes of this documentation, we'll just use an in memory structure. Also, we'll just implement the AddPet and FindPets endpoints, the others can be left unimplemented.

var pets = make(map[int64]*Pet)

var petsLock = &sync.Mutex{}

// AddPet -
func (s *DefaultApiService) AddPet(ctx context.Context, pet NewPet) (ImplResponse, error) {

	petsLock.Lock()
	defer petsLock.Unlock()

	pets[pet.Id] = &Pet{
		Id:   pet.Id,
		Name: pet.Name,
		Tag:  pet.Tag,
	}

	return Response(201, pets[pet.Id]), nil
}

func (s *DefaultApiService) FindPets(ctx context.Context, tags []string, limit int32) (ImplResponse, error) {
	values := []*Pet{}
	for _, value := range pets {
		values = append(values, value)
	}
	return Response(200, values), nil

}

Now you start your server with

go run main.go

Use your favourite HTTP client, and see that this thing is working!

Generate TypeScript client code

For the purpose of this demonstration, I generate a React application using create-react-app. The code structure looks like this:

frontend
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── ...etc
├── src
│   ├── App.tsx
│   └── ..etc
├── tsconfig.json

We generate our boilerplate with:

openapi-generator-cli generate -g typescript-fetch -o ./frontend/src/generated -i ./spec/petstore-separate/spec/swagger.json 

That is, we'll put all our generated code into src/generated.

Our generated code looks like this:

frontend/src/generated
├── apis
│   ├── DefaultApi.ts
│   └── index.ts
├── index.ts
├── models
│   ├── ModelError.ts
│   ├── NewPet.ts
│   ├── NewPetAllOf.ts
│   ├── Pet.ts
│   └── index.ts
└── runtime.ts

We don't need to edit any of this.

We instantiate an instance of our API client with:

export const petsApi = new DefaultApi(new Configuration({
    basePath: "/api"
})); 

Note that this configuration object is where we can customise the behaviour of the fetch call - for example for adding authorization credentials/headers.

We can use this (fully typed!) API client like this:

const pets = await  petsApi.findPets(); 

await petsApi.addPet({ pet: {
    id: 1, 
    name: "Foo", 
    tags: "bar"
}}); 

Very good!

Next Steps

We've demonstrated basic usage of OpenAPI generators to create boilerplate for us.

In future posts we'll delve deeper into how useful this approach is in practice.

For example: