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)
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:
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
- [1]: OpenAPI V3 Specification https://spec.openapis.org/oas/latest.html