OpenAPI is a specification for documenting HTTP APIs for both humans and machines to consume. As OpenAPI is a specification, it is language agnostic. OpenAPI relies on generators for translating the specification. There’s more than just documentation that’s generated. Generators also create language-specific interfaces, tooling, and contracts. In some ways the OpenAPI pattern reminds me of either protobuf with gRPC or ORM schema-first design. As a result, a declarative API is created by the tooling.

By the end of this post you’ll have:

  • A working Go http server generated from an OpenAPI specification.
  • A Python http client generated from the same specification and authenticates with basic auth.
  • Insight into common OpenAPI pitfalls and how to avoid them.
[openapi.yaml]
     ↓
+--------------+
| oapi-codegen | ---> [Go Server]
+--------------+
     ↓
+-----------------------+
| openapi-python-client | ---> [Python Client]
+-----------------------+

If you would like to follow along, a complete code example can be downloaded and extracted into a temporary working directory.

Generators

Because generators are consuming the specification, the OpenAPI version is determined by what the generators support.

For example, a popular Go generator is oapi-codegen and supports OpenAPI 3.0. Where a popular Python generator named openapi-python-client can support both OpenAPI 3.0 and 3.1 specifications.

Generators can be downloaded and managed as part of the languages tooling. For Go, the oapi-codegen generator is managed with Go modules and invoked with go tool oapi-codegen. With Python, creating a virtual environment, using pip install openapi-python-client, and pip freeze > requirements.txt will work nicely.

OpenAPI Schema

At first it wasn’t clear to me on how to get started with OpenAPI or what the benefits were. This is even after reviewing the OpenAPI schema documentation for 3.0.3.

To get started one needs to create a specification. A very minimal specification meeting the 3.0.x requirements is listed below. It’s not a very interesting example as endpoints in the application server aren’t defined, but it shows how minimal a specification can be that meets schema requirements.

openapi: "3.0.3"
info:
  version: 1.0.0
  title: My Contrived Server
paths:

Let’s get started by extending the simple example defining a path named /status. It will return a 200 response code with a JSON resonse.

paths:
  /status:
    get:
      responses:
        '200':
          description: Get status of the application server
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/status'

The JSON response is documented in a separate YAML block named components. It defines the response containing a JSON map containing the keys “state” and “message”, both of which have a string value.

components: 
    schemas:
      status:
        type: object
        properties:
          state:
            type: string
            example: "GOOD"
          message:
            type: string
            example: "App running within parameters"

OpenAPI supports tags, which let you group related endpoints. This example creates a data grouping and puts create_bucket in the group.

tags:
  - name: data
    description:  data manipulation endpoints

paths:
    /create_bucket:
      post:
        tags:
            - data
        requestBody:
            required: true
            content:
            application/json:
                schema:
                $ref: '#/components/schemas/create_bucket'
        responses:
            '200':
            description: Create a storage object

The OpenAPI specification also provides a definition for authentication to the web application.

components:
    securitySchemes:
      basicAuth:
        type: http
        scheme: basic
        description: Endpoints protected by basic auth base64 encoded credentials.

paths:
    /status:
        get:
            security:
                - basicAuth: []
        responses:
            '200':
            description: Get status of the application server
            content:
                application/json:
                schema:
                    $ref: '#/components/schemas/status'

Earlier I mentioned the generators will create interface files. Declarations which are considered middleware like authentication or logging are out of scope for OpenAPI. In this example, the security entries are there to document that the endpoints require basic authentication.

Generate Server Interfaces (Go)

The server walkthrough presumes one has both Make and Go installed, and the example code (tar.gz file) has been downloaded and extracted into a temp/work directory.

  • Download the Go dependencies, including oapi-codegen, by running make tidy.
  • Generate the server interfaces by running make server-codegen, which calls go tool oapi-codegen.

Feel free to inspect the api/http.gen.go file before proceeding. You’ll see it contains an interface named ServerInterface, which has the GetStatus or PostStatus endpoints from the OpenAPI specification. http.gen.go also contains a struct named Status that was defined from components -> schema -> status.

type Status struct {
	Message string `json:"message"`
	State   string `json:"state"`
}

To see the working application server, run make server-run.

The server has Basic Auth enabled with hardcoded credentials. The user is “alice” and the password “mySecretPW”. Curl can be used to see the response.

% curl --basic -u alice:mySecretPW  http://localhost:8080/status
{"message":"Initializing","state":"Unknown"}

Generate Client Interfaces (Python)

This is where OpenAPI really shines. I was able to use a generator to create Python libraries to be used by the client implementation code. The walkthrough presumes a recent version of Python3 and pip are installed.

First, create a virtual environment and install the openapi-python-client dependencies. This shell snippet presumes the current working directory is already hello_openapi.

% python3 -mvenv $PWD/.venv
% source $PWD/.venv/bin/activate
% pip install -r requirements.txt

Then run make client-codegen to build the Python client libraries located in cmd/client/my_contrived_server.

Generating the client was easy, but figuring out how to pass authentication took some trial and error. I eventually realized that the token is just a base64-encoded username:password string, and the prefix should be set to Basic.

client = AuthenticatedClient(
    base_url="http://127.0.0.1:8080",
    headers={"Content-Type": "application/json", "Accept": "application/json"},
    token="YWxpY2U6bXlTZWNyZXRQVw==",  # Token string is a base64 string containing alice:mySecretPW
    prefix="Basic"
)

To see the client in action, run make client-run. Also take a look at cmd/client/client.py. It only took a few lines of python code to implement what the openapi-python-client generator had created.

Gotchas & Lessons Learned

One issue I have with OpenAPI is the illusion of simplicty. When I first started working with OpenAPI, I noticed the Status struct had keys referencing a pointer of strings which wasn’t ideal.

type Status struct {
	Message *string `json:"message"`
	State   *string `json:"state"`
}

It took some fiddling with the OpenAPI specification to make the generator use strings instead of pointers to strings. Adding ‘required’ to the schema made the generator do what I wanted.

components:
      status:
        type: object
        properties:
          state:
            type: string
            example: "GOOD"
          message:
            type: string
            example: "App within parameters"
        required:
          - state
          - message

Another issue was not knowing that in Paths, GETs should have a responses entry and POSTS should have a RequestBody entry. It makes sense, but it wasn’t obvious to me when stumbling through hello-world.

The main takeaway? Always inspect the generated code. If something doesn’t look right, like unexpected pointers or missing method args, chances are your spec needs tweaking.

Wrapping Up

Even though I hit some issues with a fairly simple example, I’m going to continue using OpenAPI specifcations. Being able to easily generate client code in a different language was a real win. And let’s not forget the free API documentation and contract definitions which comes with OpenAPI. I have a more complex OpenAPI project coming up. I’m sure I’ll have more notes (and probably more gotchas) to share. Stay tuned.

If you’ve had similar struggles with OpenAPI or tips for improving schema design, I’d love to hear them on Bluesky Social.