Contents

Increase development productivity with OpenAPI

It has been a while since my last post. Much has happened since and I just haven’t found the time to write new blog posts. I will try my best to change this in 2023

Intro

In this post we will talk about OpenAPI and how it can be used to increase productivity. I will show how OpenAPI schemas can be used to generate API client code, so you can focus on business logic of your app.

Prerequisites

Basic knowledge on how Django works.

Technology used

  • Django
  • DRF (django rest framework) docs
  • drf-spectacular (python package for generating openapi schema) docs
  • openapi-generator docs

What is OpenApi?

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic. When properly defined via OpenAPI, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. Similar to what interface descriptions have done for lower-level programming, the OpenAPI Specification removes guesswork in calling a service.[1]

In other words, the result of OAS will usually be a json or yaml schema representing your:

  • endpoints,
  • schemas of request/response models
  • response types
  • documentation
  • authentication details
  • and other information.

This can be used for code generation and/or if endpoints are public, also shared to your customers, so they can easily use your services. Examples will be shown later in the post

OpenAPI is technology agnostic. Many major frameworks include support for generating OpenAPI schemas.

How can all this increase my productivity?

Before I started using OpenAPI schemas and code generation, I used to hate one part of full stack development, and that is writing API clients. It’s tedious, repetitive and prone to errors. You are essentially copying your backend presentation models to your client. If you need to write client with many endpoints, the code can quickly become inconsistent. It’s even worse, if there are multiple engineers working on the project.

Code generation

If you have a OpenAPI schema, you can throw it at a OpenAPI code generator. There are many open source solutions for this and the one that I prefer to use is OpenApi Generator.

It can generate clients and servers in many popular languages in different variations. You can generate typescript client with axios or fetch and then use the code directly in your front-end application. If you use flutter for native mobile apps, you can generate a client in dart language.

Benefits

With code generation, you will get consistent api clients. Generators will usually create models/interfaces which are 1-1 to your backend representation models. This can provide better type safety.

But most important, you can now focus on your business logic and stop wasting time with writing additional code for API clients.

If your OpenAPI schema is well documented, it can be more efficiently used by you and your customers. They can easily understand your public API and also generate their own code.

There is nothing more painful than integrating poorly documented API endpoints.

Swagger UI

Swagger UI is a tool which can take your OpenAPI schema and generate wonderful user interface. It allows you to send requests to the server and modify payloads. You can check a demo swagger page on Swagger demo

One alternative to Swagger is Redoc, which serves a similar purpose. Check the demo on Redoc demo.

Building a fullstack environment with OpenAPI

In this example I will show you, how OpenAPI was used in the Slovenian project for saving food waste. The name of the project is Krožnik (which translates to plate in english). If you are interested in this project and want to save food going to waste you can visit the landing page Krožnik.

Disclaimer: The project is still under development.

Krožnik is a fullstack application consisting of 3 main components:

  • Backend (Django & DRF)
  • Backoffice frontend page for merchants (Nuxt 3 with typescript)
  • Mobile app for customers (Flutter)

/images/Kroznik.png

In our case, we wanted to generate API client code in typescript for backoffice and dart for flutter.

We will not go into details of our Krožnik project. It is only used as a reference for examples.

Backend project

Setup dependencies

Create a Django project using the django-admin command.

django-admin startproject backend

Add django rest framework and drf-spectacular

pip install djangorestframework
pip install drf-spectacular

Add both libraries to INSTALLED_APPS

INSTALLED_APPS = [
    # ALL YOUR APPS
    'rest_framework',
    'drf_spectacular',
]

Add drf-spectacular urls for swagger and OpenApi schema

urlpatterns = [
    path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
    path(
        "api/schema/swagger-ui/",
        SpectacularSwaggerView.as_view(url_name="schema"),
        name="swagger-ui",
    ),
]

You can read more about Django rest framework and drf-spectacular in their docs. The latter has many examples how to add additional information to generated OpenAPI schema.

Add example endpoint

I will create a simple endpoint for fetching details about an authenticated user.

Example serializer and view:

class AccountType(models.TextChoices):
    RETAIL = "retail"
    COMPANY = "company"


class UserInfoSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(read_only=True)
    email = serializers.EmailField(read_only=True)
    account_type = serializers.ChoiceField(choices=AccountType, read_only=True)
    phone = serializers.CharField(read_only=True)


class UserInfoView(APIView):
    permission_classes = (IsAuthenticated,)

    @extend_schema(
        responses={
            200: UserInfoSerializer,
            401: OpenApiResponse(description="Authentication details were not provided."),
        },
        operation_id="getUserData",
    )
    def get(self, request: Request):
        # If docs are provided, it will be used
        """
        Returns details of user
        """
        user: CustomUser = request.user
        serializer = UserInfoSerializer(user)
        return Response(serializer.data)

Url for this endpoint is /api/auth/user/me

Swagger

We set the swagger url to /api/schema/swagger-ui/. If we visit this url on localhost, we get the following site: /images/kroznik_openapi.png

We can also access the OpenAPI schema on endpoint /api/schema/. The result is a yaml file

openapi: 3.0.3
info:
  title: Kroznik OpenApi
  version: 1.0.0
paths:
  /api/auth/user/me:
    get:
      operationId: getUserData
      description: Returns details of user
      tags:
        - auth
      security:
        - cookieAuth: [ ]
        - JwtAuthScheme: [ ]
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfo'
          description: ''
        '401':
          description: Authentication details were not provided.
components:
  schemas:
    AccountTypeEnum:
      enum:
        - retail
        - company
      type: string
    UserInfo:
      type: object
      properties:
        id:
          type: integer
          readOnly: true
        name:
          type: string
          readOnly: true
        email:
          type: string
          format: email
          readOnly: true
        account_type:
          allOf:
            - $ref: '#/components/schemas/AccountTypeEnum'
          readOnly: true
        phone:
          type: string
          readOnly: true
          nullable: true
      required:
        - account_type
        - email
        - id
        - name
        - phone
  securitySchemes:
    JwtAuthScheme:
      type: apiKey
      in: header
      name: Authorization
      bearerFormat: bearer
      description: Bearer token
    cookieAuth:
      type: apiKey
      in: cookie
      name: sessionid

Schemas can quickly grow, so for demo purposes, we will stick to one endpoint.

If we take a quick look, we can see paths and components. The former will describe all endpoints, available http methods, responses and request payloads for each method etc. The latter describes all schemas of payloads for requests and responses. OpenAPI code generators will use this information to generate client code.

Generating client code

Like mentioned before, we will use openapi-generator to generate client code. Check the installation methods. If you are on a Mac machine, I prefer using homebrew.

Check the support for generated languages.

For frontend applications I like to use typescript-axios and for flutter dart generator.

For convenience purposes I wrote a shell file generate_openapi.sh

# Delete previously generated code
rm -rf generated_ts_client generated_dart_client

# Fetch OpenApi schema and save it to openapi_schema.json
curl http://localhost:8000/api/schema/ > openapi_schema.json

# Generate code.
# Notes on parameters:
# -i: location of schema
# -g: client generator name
# -o: output folder
openapi-generator generate -i openapi_schema.json -g dart -o generated_dart_client
openapi-generator generate -i openapi_schema.json -g typescript-axios -o generated_ts_client

Each generator has support for additional parameters. Check the docs for those.

Generated code will contain some files, which are not needed. You can copy the needed files to (manually or with a bash script) your locations of choice.

For typescript-axios I copy code from the following files:

  • generated_ts_client/api.ts
  • generated_ts_client/base.ts
  • generated_ts_client/common.ts
  • generated_ts_client/configuration.ts
  • generated_ts_client/index.ts

for dart:

  • generated_dart_client/lib/
  • generated_dart_client/doc/ (Copy if you want to have markdown docs in your mobile app source code)

Using the generated typescript code

With generated typescript code, we can call the user data endpoint.

const response = await new AuthApi(new Configuration(), '', axiosInstanceAuth).getUserData()

I usually create my own axios instance, so interceptors can be added to it. In this case, axiosInstanceAuth is my own axios instance.

If you look at the Django view, you will see the operation_id. This is used for naming the generated function. In the example above that is getUserData. If no operation_id is provided, openapi generator will generate the name on its own.

Using the generated dart code

Same goes for flutter.

var bearerAuth = HttpBearerAuth();
bearerAuth.accessToken = "access token";

var client = ApiClient(
    basePath: "https://your-server.com",
    authentication: bearerAuth
);
var user = await AuthApi(client).getUserData();

Variable user will be of type UserInfo (see OpenAPI schema) and will contain all serialization/deserialization methods, so you don’t have to deal with raw request/response.

Conclusion

Using OpenAPI can significantly reduce the time to write client API code, so you can focus on code that performs business logic.

Resources