DEV Community

Cover image for How to write an application using API-First development
Thiago Pacheco
Thiago Pacheco

Posted on • Edited on

How to write an application using API-First development

API first approach is a software development technique in which an API (Application Programming Interface) is designed first and then the implementation of the API is done later. This approach is different from the traditional approach of building an application, where the business logic or even the UI (User Interface) is given more importance and the API is designed, developed and documented later.

In this post, we are going to cover why API-First is a good practice and how you can implement it in Python.
Additionally, we are going to cover practices such as: how to define your API documentation and use TDD when developing it.

Why API-First?

The API first approach has several advantages over the traditional approach. Here are some of the benefits of using an API first approach:

  1. Better Documentation:
    Since the API is developed first, it is easier to document it properly. This makes it easier for developers to understand how to use the interface being proposed and also makes it easier for other teams to use the API.

  2. Improved Integration:
    With the API first approach, it is easier to integrate different systems and applications. This is because the API is designed to be used by other systems and applications from the beginning.

  3. Reusable Components:
    The API first approach allows for the development of reusable components that can be used in different applications. This saves time and effort as the same component can be used in multiple applications.

  4. Faster Development:
    With the API first approach, the UI development can be done in parallel with the API development. This allows for faster development as both the API and the UI can be developed simultaneously.

  5. Improved Security:
    Since the API is developed first, it is easier to implement security measures at the API level. This makes the application more secure as the API acts as a layer of protection between the application and external systems.

We can go a step further and use TDD along with this practice, which allows us to make sure that we are building an application that fully complies with the plan we have and we make sure that the plan is properly tested all the way through.

To put the API first approach into practice with Python, you can use a library called Connexion. Connexion is a Python library that allows you to define your API using OpenAPI specification and then automatically generates a Flask-based application.

How can we put this in practice?

First, make sure you have all the required tools for this tutorial installed:

Requirements:

  • Python 3^
  • Poetry
  • A code editor of your choice

Now that you have all the necessary tools, let's go ahead and start building this app!

Here are the steps to follow to implement the API first approach using Connexion and Python:

1. Create a new app with Poetry:

poetry new habit_tracker
cd habit_tracker
poetry add connexion
Enter fullscreen mode Exit fullscreen mode

2. Define the API using OpenAPI specification:

This specification defines the various endpoints of your API, the request and response parameters, and other details of your API. You can use tools like Swagger Editor to define your API using the OpenAPI specification.

Note: To develop fully using TDD we would start by defining the API tests first and slowly implement our code, but this deserves an article on it's own so let's keep it as simple as possible

Here is an example of an OpenAPI specification file for this Habit Tracker application:

openapi: 3.0.0
info:
  title: habits.openapi
  version: '1.0'
  license:
    name: MIT
  contact:
    name: Thiago Pacheco
    url: pacheco.io
    email: hi@pacheco.io
  description: A simple API to manage Habits
servers:
  - url: 'http://localhost:5000'
paths:
  /:
    parameters: []
    get:
      summary: List Habits
      tags:
        - Habits
      responses:
        '200':
          description: List of Habits
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Habit'
              examples:
                Example 1:
                  value:
                    - id: 1
                      description: Learn how to use API-First
                      user_id: 1
                      created_at: '2023-01-01 10:00'
                      updated_at: '2023-01-01 10:00'
      operationId: habit_tracker.list
      description: Retrieve the list of habits of a given user
    post:
      summary: Create a Habit
      operationId: habit_tracker.create
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Habit'
              examples:
                Example 1:
                  value:
                    id: 1
                    description: Learn how to use API-First
                    user_id: 1
                    created_at: '2023-01-01 10:00'
                    updated_at: '2023-01-01 10:00'
      description: ''
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HabitCreate'
            examples:
              Example 1:
                value:
                  description: Learn how to use API-First
                  user_id: 1
        description: ''
      tags:
        - Habits
  '/{habit_id}':
    parameters:
      - schema:
          type: integer
        name: habit_id
        in: path
        required: true
    get:
      summary: Get a Habit by ID
      operationId: habit_tracker.get
      responses:
        '200':
          description: OK
        '404':
          description: Not Found
      tags:
        - Habits
    put:
      summary: Update a Habit
      operationId: habit_tracker.update
      responses:
        '202':
          description: Accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Habit'
        '404':
          description: Not Found
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HabitCreate'
    delete:
      summary: Delete a habit
      operationId: delete-habit_id
      responses:
        '204':
          description: No Content
        '404':
          description: Not Found
components:
  schemas:
    HabitCreate:
      title: HabitCreate
      type: object
      x-examples:
        Example 1:
          description: Learn how to use API-First
          user_id: 1
      properties:
        description:
          type: string
        user_id:
          type: integer
    Habit:
      title: Habit
      type: object
      properties:
        id:
          type: integer
        description:
          type: string
        user_id:
          type: integer
        created_at:
          type: string
        updated_at:
          type: string
      x-examples:
        Example 1:
          id: 1
          description: Learn how to use API-First
          user_id: 1
          created_at: '2023-01-01 10:00'
          updated_at: '2023-01-01 10:00'

Enter fullscreen mode Exit fullscreen mode

3. Generate the Flask application using Connexion:

Once you have defined your API using the OpenAPI specification, you can use Connexion to generate a Flask-based application. Connexion reads the OpenAPI specification and generates the necessary routes, request and response parsing, and error handling for your API.

Let's create a quick test with our expectation

# tests/test_habit_tracker.py

from habit_tracker.app import create_app


def test_can_create_app():
    app = create_app()
    assert app is not None

Enter fullscreen mode Exit fullscreen mode

Let's add the logic to make the test pass.

Create the initialization code for the app:

Under /habit_tracker folder, create a file named app.py with the following content:

# /habit_tracker/app.py
from flask import Flask
import connexion


def create_app() -> Flask:
    app = connexion.FlaskApp(__name__, specification_dir='../openapi/')
    app.add_api('habit.openapi.yaml')
    return app.app
Enter fullscreen mode Exit fullscreen mode

Create a file named resources.py under /habit_tracker with the following content:

# /habit_tracker/resources.py
from typing import Dict

from flask import jsonify


def list_habits():
    """
    Return a list of habits
    """


def create_habit(body: Dict):
    """
    Create a new habit
    """


def get_habit_by_id(habit_id: int):
    """
    Get a habit by ID
    """


def update_habit(habit_id: int, body: Dict):
    """
    Update a habit
    """


def delete_habit(habit_id: int):
    """
    Delete a habit
    """
Enter fullscreen mode Exit fullscreen mode

Quick run the tests to make sure it is passing:

poetry run pytest tests/
Enter fullscreen mode Exit fullscreen mode

If it does not pass, take a quick look again at the previous steps to make sure they were all complete.

4. Implement the API logic:

Once you have the generated Flask application, you can implement the logic for each endpoint of your API. You can use Python functions to handle the request and return the appropriate response.

Note that each endpoint defined in the OpenAPI file has a operationId property. This property points to where your function is supposed to be defined inside the Flask application.

Let's start by adding some tests for the usage of our expected endpoints:

# tests/test_habit_tracker.py
import pytest
from habit_tracker.app import create_app

# ...


@pytest.fixture
def client():
    app = create_app()
    client = app.test_client()
    return client


def test_list_habits(client):
    response = client.get("/")
    assert response.status_code == 200
    assert response.json == [
        {
            "id": 1,
            "description": "Drink 8 glasses of water a day",
            "user_id": 1,
            "created_at": "2020-01-01 12:00",
            "updated_at": "2020-01-01 12:00",
        },
    ]


def test_create_habit(client):
    payload = {
        "description": "Drink 8 glasses of water a day",
        "user_id": 1,
    }
    response = client.post("/", json=payload)
    assert response.status_code == 201
    assert response.json == {
        "id": 1,
        "description": "Drink 8 glasses of water a day",
        "user_id": 1,
        "created_at": "2020-01-01 12:00",
        "updated_at": "2020-01-01 12:00",
    }


def test_get_habit_by_id(client):
    response = client.get("/1")
    assert response.status_code == 200
    assert response.json == {
        "id": 1,
        "description": "Drink 8 glasses of water a day",
        "user_id": 1,
        "created_at": "2020-01-01 12:00",
        "updated_at": "2020-01-01 12:00",
    }


def test_update_habit(client):
    payload = {
        "description": "Workout 45 minutes a day",
        "user_id": 1,
    }
    response = client.put("/1", json=payload)
    assert response.status_code == 202
    assert response.json == {
        "id": 1,
        "description": "Workout 45 minutes a day",
        "user_id": 1,
        "created_at": "2020-01-01 12:00",
        "updated_at": "2020-01-01 12:00",
    }


def test_delete_habit(client):
    response = client.delete("/1")
    assert response.status_code == 204
Enter fullscreen mode Exit fullscreen mode

Now, let's make the tests pass by implementing the API logic:

# /habit_tracker/resources.py

from typing import Dict
from flask import jsonify


def list_habits():
    """
    Return a list of habits
    """
    return jsonify([
        {
            "id": 1,
            "description": "Drink 8 glasses of water a day",
            "user_id": 1,
            "created_at": "2020-01-01 12:00",
            "updated_at": "2020-01-01 12:00",
        },
    ])


def create_habit(body: Dict):
    """
    Create a new habit
    """
    return jsonify({
        "id": 1,
        "description": body["description"],
        "user_id": body["user_id"],
        "created_at": "2020-01-01 12:00",
        "updated_at": "2020-01-01 12:00",
    }), 201


def get_habit_by_id(habit_id: int):
    """
    Get a habit by ID
    """
    return jsonify({
        "id": habit_id,
        "description": "Drink 8 glasses of water a day",
        "user_id": 1,
        "created_at": "2020-01-01 12:00",
        "updated_at": "2020-01-01 12:00",
    })


def update_habit(habit_id: int, body: Dict):
    """
    Update a habit
    """
    return jsonify({
        "id": habit_id,
        "description": body["description"],
        "user_id": body["user_id"],
        "created_at": "2020-01-01 12:00",
        "updated_at": "2020-01-01 12:00",
    }), 202


def delete_habit(habit_id: int):
    """
    Delete a habit
    """

Enter fullscreen mode Exit fullscreen mode

Every test should be passing now:

poetry run pytest tests/
Enter fullscreen mode Exit fullscreen mode

5. Run the Flask application:

To run the application, let's create a main.py file under /habit_tracker and run with poetry:

# /habit_tracker/main.py

from habit_tracker.app import create_app


def run():
    app = create_app()
    app.run()

Enter fullscreen mode Exit fullscreen mode

Add a poetry script to run the application.

# pyproject.toml
# ...
[tool.poetry.scripts]
start = "habit_tracker.main:run"
# ...

Enter fullscreen mode Exit fullscreen mode

You can then run the Flask application to start the API server and make the API available for use.

poetry run start
Enter fullscreen mode Exit fullscreen mode

Go ahead and test your endpoints using your favourite HTTP client. You can use insomnia or Postman for example.

Additional (Optional) steps

Additionally, you can also include a Swagger UI by adding the following extra package to your project:

poetry add connexion[swagger-ui]
Enter fullscreen mode Exit fullscreen mode

With this package included, you can rerun your application and go to the url http://localhost:5000/ui

A nice and shiny Swagger UI will be available to you!

Using the API first approach with Connexion and Python allows you to easily design and implement an API. You can use the OpenAPI specification to define the API and then use Connexion to generate the Flask application. This allows you to focus on the logic of your API rather than the boilerplate code required to set up the API server.

In summary, the API first approach has several advantages over the traditional approach of building an application. It allows for better documentation, improved integration, reusable components, faster development, and improved security. Therefore, it is worth considering the API first approach while building any application.

The source code can be found here.

Top comments (4)

Collapse
 
gdledsan profile image
Edmundo Sanchez

What do you think of FastAPI ?
It takes care of all the OpenAPI stuff.

Collapse
 
pacheco profile image
Thiago Pacheco

It is true that Fastapi takes care of generating the specs, but that is not an API-First approach and fastapi doesn’t really support an approach like this. That is why I chose flask and connexion to show a way to generate the endpoints from the docs.
It is definitely possible to apply the same practice with any other technology though, this just happened to be one possible way to do it and it supports a nice automation.
I hope that answers your question.

Collapse
 
gdledsan profile image
Edmundo Sanchez

It does, I was just curious about it.
I understand API first means then design first?

Thread Thread
 
pacheco profile image
Thiago Pacheco

Yes, it means designing the api before actually implementing it