This article is a part of the series "Generating applications from an OpenAPI spec"
- Why use OpenAPI?
- Generating application and client code from an OpenAPI spec
- Generating MSW mocks from an OpenAPI spec.
- Using Postman and an OpenAPI spec for contract/blackbox tests
- Postman and OpenAPI - Automating tests
- OpenAPI and contract tests with Jest
- Blackbox contract tests - Performance testing with Jest
- Black box contract testing - third party API mocking
Generating application and client code from an OpenAPI spec
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 pet store 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
- Use the CLI tool to generate the code boilerplate for the server and client
- 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:
- How will the generated code deal with us making a mistake, and attempting to pass/return data of the wrong shape?
- How do we extend our API without clobbering our changes?
Questions? Comments? Criticisms? Get in the comments! 👇
Spotted an error? Edit this page with Github