Web APIs & Data formats

Client-Server Model
APIs
Pydantic Model
Data Mapping
Lab 4 - web service
Author

Ludovic Deneuville

Before you start

This tutorial combines explanations and code phases. The explanations should not take precedence over those of your teacher.

NoteConcepts covered
  • Explore existing web services (Manual & Swagger)
  • Build a Data Mapper to consume an external API with a different schema
  • Create your own REST API using FastAPI
  • Implement security via Tokens and data validation via Pydantic

Create your branch

For each practical session, the teacher provides a starting branch: tp<N>-start.

You will have to create a personal branch: <username>-tp<N> (e.g. camille-tp2) and work on your own branch.

To start with a clean and up-to-date codebase:

Then use the standard Git cycle: add, commit, push after each part.

1 Client-Server principle

To understand how software systems communicate, let’s step out of the terminal and into a clothing store. Imagine you are a customer trying to interact with a shop assistant; this simple interaction is the perfect metaphor for the Client-Server relationship and the world of APIs.

sequenceDiagram
    participant Client as Customer (Client)
    participant API as Shop Assistant (API)
    participant Server as Warehouse (Server)

    Note over Client, Server: READ Operations (Filtering)
    
    Client->>API: "Show me all underwear" (GET /underwear)
    API->>Server: Query all items
    Server-->>API: [Boxer, Briefs, Panties]
    API-->>Client: 200 OK (List of items)

    Client->>API: "I want pink briefs" (GET /underwear?type=briefs&color=pink)
    API->>Server: Filter collection
    Server-->>API: Not Found
    API-->>Client: 404 Not Found: "We don't carry pink briefs!"

But wait! A shop is not just for looking. If you want to actually do something-like adding new stock or changing prices-you need more than just a “viewing” request. You need to perform Write Operations.

sequenceDiagram
    participant Client as Customer (Client)
    participant API as Shop Assistant (API)
    participant Server as Warehouse (Server)

    Note over Client, Server: WRITE Operations (Targeting specific IDs)

    Client->>API: "Add a Rainbow Panties" (POST /underwear)
    Note right of Client: Body: {"type": "panties", "color": "rainbow"}
    API->>Server: Insert new record
    Server-->>API: Created (ID: 101)
    API-->>Client: 201 Created: "Added to catalog!"

    Client->>API: "Update Gold Boxer price" (PUT /underwear/99)
    Note right of Client: Body: {"price": 999.99}
    API->>Server: Update item 101
    Server-->>API: Success
    API-->>Client: 200 OK: "Price updated. French quality!"

    Client->>API: "Delete green briefs" (DELETE /underwear/42)
    API->>Server: Remove item 42
    Server-->>API: Deleted
    API-->>Client: 204 No Content: "Bye bye green briefs."

Now that anyone can add or delete items, our shop is a mess! We can’t just let anyone walk in and change everything. We need to implement Security and Permissions to protect our inventory.

sequenceDiagram
    participant Client as Customer (Client)
    participant API as Shop Assistant (API)
    participant Server as Warehouse (Server)
    Note over Client, Server: Security & Permissions

    Client->>API: "Get VIP items" (GET /vip-items)
    Note right of Client: Missing Token
    API-->>Client: 401 Unauthorized: "Reserved for members!"

    Client->>API: "Delete an item" (DELETE /underwear/101)
    Note right of Client: x-auth-token: r2D2c3Po Valid, but wrong Role
    API-->>Client: 403 Forbidden: "You are a customer, not the manager!"

2 Exploring the Web (The Manual Way)

Before writing code, you must understand how to “talk” to a server. A web service is just an application waiting for HTTP requests.

2.1 Reminder

In the first lab, we explained the difference between an API and an interface.

The Front-end is the “face” running on the user’s device, while the Back-end is the “brain” running on a remote server.

The UI is what the user sees; user actions are sent to the back-end via the API.

Concept Primary User Synonyms Main Role
API Machines / Software Webservice, Endpoint Enables two software systems to communicate
Back-end Server / Database Server-side The server-side logic, API implementation.
UI (User Interface) Human Graphical Interface The visual elements (buttons, menus) that a user interacts with
Front-end Human Client The client-side. Code running in the browser that renders and manages the UI

2.2 Quick Browser Check

The simplest way to interact with an API is via your browser (Firefox, Chrome, etc.). However, browsers are limited to GET requests (read data).

Lets explore https://swapi.info/api/

    • What is the data structure?
    • What is the data structure?

Another API https://pokeapi.co/api/v2/

https://swapi.info/api/people -> list[dict] https://swapi.info/api/people/51 -> Mace Windu (dict) https://swapi.info/api/planets/42 https://swapi.info/api/films/2 -> planets

https://pokeapi.co/api/v2/pokemon?limit=10&offset=50 https://pokeapi.co/api/v2/pokemon/54/ -> psyduck, type=water https://pokeapi.co/api/v2/type/11/ -> type=water https://pokeapi.co/api/v2/pokemon/114 -> type=grass

3 Exploring your project’s API

Modern APIs provide a “manual” that is interactive: Swagger (OpenAPI).

3.1 Using Swagger

Your current application already has a web service running.

You’ve already done this in the first Lab.

    • Select the endpoint ➡️ Try it out ➡️ Execute

Up until now, you have used GET requests to ask for information (the same goes for DELETE). But to create or modify data, you must provide information. This is called the Request Body.

When using POST or PUT, you don’t just send a URL; you send a JSON object containing the details of the resource.

{
  "username": "new_player",
  "email": "new@player.com",
  "elo": 1200
}
    • add 100 points to its elo, change suffix mail from io to fr
    • Where is the password?
    • In the response, copy the token
Note

With each request, you’ll notice the curl command if you want to run it from the service’s terminal.

It’s all well and good to marvel at all that JSON code, but it’s usually machines and programs that consume it.

3.2 API Clients

Having a Swagger is very convenient and easy to use. However, if one isn’t available, there are other ways to communicate with a web service.

Create a new player using:

4 The Client Challenge (Data Mapping)

Now, we will move to a more advanced task. Your back-end will act as a client. You need to fetch game data from an external source and integrate it into your engine.

WarningThe Schema Mismatch

The external API does not use your internal names. It is a common real-world problem. You must bridge the gap!

4.1 The Mission

Your application needs to retrieve a list of games from a web service.

Your task is to retrieve this data and then convert it into a suitable format. Here is an example

  1. Call the web service
  2. Check the response
  3. Convert JSON response into a list of Games

4.2 The server

It’s not very difficult to create a fake API, for example:

    • uvx json_server data/game.json 5555

4.3 Your turn

This client must fetch data from an external API and transform it into your Game business objects.

Below is the data available on this external API:

/games
[
  {
    "id": "1",
    "players_list": ["Batricia", "Junior"],
    "winner_name": "Batricia",
    "location_name": "Arena 1",
    "duration_seconds": 120,
    "mode_type": "coinflip"
  },
  {
    "id": "2",
    "players_list": ["Maurice", "Gilbert"],
    "winner_name": "Gilbert",
    "location_name": "Secret Cave",
    "duration_seconds": 300,
    "mode_type": "dice"
  }
]

This is a list [element1, element2] of dictionaries {key1: value1, key2: value2}.

Important

The goal is to convert this dictionary list into a Game list by:

  • iterating through each element of the list one by one
  • creating a Game object from the available data in the dictionary

⚠️ A dictionary is not an object, so attributes are not required for every element.

    • use package requests
    • save the response into a variable r
    • use method r.raise_for_status() or attribute r.status_code
TipTest it manually
sandbox.py
from client.game_client import GameClient

client = GameClient()
games = client.get_games()
print(f"{len(games)} games loaded:")
for g in games:
    print(f"- {g}")

uv run --project backend python backend/sandbox.py

In this section, you were a client of an API; next you’ll become the provider by creating your own endpoints.

5 Building your own API

Finally, you will become the Server. You will expose your Game data via a professional REST API using FastAPI.

It’s pretty easy to create your own API with FastAPI.

5.1 Your First Endpoint

NoteThe Application Entry Point

What’s in this file? It is the Orchestrator of your application:

  • Routing Assembly: It connects modular Routers (Controllers) to the main FastAPI instance
  • Utility Management: It provides administrative endpoints like database resets or documentation redirects
  • Server Execution: It launches the ASGI server (Uvicorn) to handle incoming HTTP requests

You’ll notice that every endpoint prefixed with /game will be handled in the game_controller.py file.

At the moment, we have an endpoint to POST a Game.

Step-by-step, let’s create one to GET the Game by Player.

@router.get("/", tags=["Games"])
async def get_mock_games():
    return [
        {"id_game": 1, "mode": "coinflip", "winner": "Miguel"},
        {"id_game": 2, "mode": "dice", "winner": "Batricia"}
    ]

OK, that’s a start. Now, let’s aim to display all Games played by a specific Player. We’ll modifying the previous method.

    • to have this endpont: GET /game?id_player=<?>
    • for your first try, just add this ID into the returned JSON (new key-value pair)

If you want this parameter to be optional, you must specify a default value for it in the method signature (e.g., None).

A better way is to use a hierarchical URI that semantically expresses that games are a sub-resource belonging to a specific player.

player_controller.py
# GET /player/{id_player}/games
@app.get("/players/{id_player}/games", response_model=list[GameModel])
def get_games(id_player, service: game_service=Depends(get_game_service)):
    if ... # Player don't exists
        raise ...
    return ... # Call a service

Finally, instead of returning raw JSON data, let’s now retrieve the actual list of games played by the player with that ID.

    • If not, implement one.
    • If not, implement one.

Here is the route:

sequenceDiagram
    participant Client
    participant Controller as GameController
    participant Service as GameService
    participant DAO as GameDAO
    participant DB as Database

    Client->>Controller: GET /game?id_player=1
    Note over Controller: Validates request & params
    Controller->>Service: find_all_by_player(1)
    Service->>DAO: find_all_by_player(1)
    DAO->>DB: SELECT * <br> FROM game <br> WHERE id_player = 1
    DB-->>DAO: Returns rows
    DAO-->>Service: list[Game]
    Service-->>Controller: list[Game]
    Controller-->>Client: 200 OK (JSON list)

Great, it works.

But do you notice anything that seems wrong?

What if our Game table contained confidential information?

How can we ensure that it isn’t exposed?

Fortunately, there is a solution for managing data input and output!

5.2 Data Validation

When you expose an endpoint, you don’t have to reveal all the data you have. It may be a good idea to hide passwords, tokens, and so on…

Conversely, when your endpoint receives data (e.g., form), you cannot trust the client. A user might send a string where you expect an integer, or forget a mandatory field. To prevent this “garbage” from entering your system, we use Pydantic models.

A Pydantic model acts as a Data Contract: it defines exactly what the data should look like. If the incoming JSON doesn’t match the contract, FastAPI will automatically reject the request with a “422 Unprocessable Entity” error.

5.2.1 Define a Model

Example: in the player_model.py file, we define how a Player should look when communicating with the API:

from pydantic import BaseModel, EmailStr, Field

class PlayerModel(BaseModel):
    id_player: int | None = None
    username: str
    password: str = Field(..., min_length=35)
    elo: int
    email: EmailStr
    pokemon_fan: bool

Many types are available :

  • basic: int, float, str, bool, etc.
  • advanced: FilePath, HttpUrl, EmailStr, PastDate, Color, PositiveInt, etc.

It is also possible to add your own constraints.

5.2.2 Use it in the Controller

Let’s look at the create_player() method in player_controller.py.

@router.post("/", tags=["Players"])
async def create_player(p: PlayerModel, player_service=Depends(get_player_service)):
    # ...
    player = player_service.create(p.username, p.password, p.elo, p.email, p.pokemon_fan)

Here is the magic happening behind the scenes:

  1. The Request: a client sends request to the endpoint with a JSON body: {"username": "bob", "email": "bob@email.com", ...}.
  2. Validation: FastAPI looks at your PlayerModel. It checks:
  • Is username a string?
  • Is elo an integer?
  • Is email a valid email format?
  • Are all mandatory fields present?
  1. Conversion: If everything is correct, FastAPI automatically transforms that JSON into a Python object named p.
  2. Execution: You can then access the data easily using dot notation (p.username, p.email) to pass it to your PlayerService.

If the client sends an invalid email or forgets the elo, FastAPI will stop the request immediately and return a “422 Unprocessable Entity” error. Your service code is never even executed, protecting your business logic from bad data.

There’s already a model. Let’s see how it’s used.

    • to find out who the current player is, it use the Token
    • Input: opponent and choice (heads or tails) are provided by the GamePlayModel
    • Output: the response format is a GameResponseModel

graph LR
    subgraph "Input (Client Request)"
        Req[GamePlayModel]
    end

    subgraph "Controller"
        Ctrl[play_game]
    end

    subgraph "Output (Client Response)"
        Res[GameResponseModel]
    end

    Req -->|Validates| Ctrl
    Ctrl -->|Maps to| Res

OK, cool. We already have models for Game. But are these models appropriate in our case?

We can use GameResponseModel as output, but we can do better.

5.3 Multiple Models

The more, the better.

A common mistake is to use the same model for every single operation. However, in a professional API, the data you receive is often different from the data you send or the data you use for specific actions.

Think about the Player logic. Using only one model creates several issues:

  • During Registration: You need the password, but you do not have an id_player yet (the database generates it).
  • During Login: You only need to provide username and password. Including the email or the elo is a waste of bandwidth and a security risk.

To solve this, we use different models for different contexts. This is called Separation of Concerns.

In player_model.py, we define three specialized contracts to match the specific needs of our API endpoints:

  1. PlayerModel: The “Full” version. It contains all player attributes (email, elo, pokemon_fan, etc.).
  • Usage: It is used as input in player_controller.py within the create_player and update_player methods to ensure that all required profile information is validated.
  1. PlayerReadModel: Used as an output to display information about a Player.
  2. PlayerLoginModel: The “Minimal” version. It is stripped down to only the essential fields required for authentication (username and password).
  • Usage: It is used in login_controller.py within the login method. This prevents the API from requiring or exposing unnecessary data like email or elo during a simple login attempt.

Let’s get back to the Games.

The goal will be to create an endpoint to display a player’s games.

    • id_game, game_mode, description, timestamp
    • add player1, player2 and winner: use another Model (player1: somePlayerModel)
    • the output must be a list of GameReadModel

While using two models is a great start, professional APIs often use even more granular models to handle different stages of a resource’s lifecycle. You could further optimize this by implementing:

  • PlayerCreate vs PlayerUpdate: Currently, PlayerModel is used for both. However, when creating a player, you might want to require an email, but when updating a player, you might want to allow them to change only their elo without re-sending their username.
  • PlayerResponse: A model for sending data back to the client that explicitly excludes the password field, ensuring sensitive information never leaves the server.
  • PlayerSearch: A specialized model for filtering players (e.g., searching only by username or pokemon_fan status).

5.4 Security: Token-based Authentication

Bonus

A professional API is rarely wide open. To protect your data, you will implement Token-based authentication.

Instead of asking for a username and password on every single request, the client sends a “secret key” (the Token) in the HTTP Headers. If the token is missing or incorrect, the server rejects the request immediately.

WarningHow to implement it

You will implement a dependency that checks for a specific header. If the header is invalid, you must raise an HTTPException with a 401 Unauthorized status code.

The Security Contract:

  • Header Key: X-Auth-Token
  • Valid Value: secret-key-123

Example of a protected endpoint in your controller:

@router.post("/", response_model=GameResponseModel, tags=["Games"])
def play_game(
    req: GamePlayModel, game_service=Depends(get_game_service), current_player=Depends(verify_token)
):
    ...

Function verify_token() from file utils/security.py will:

  • Get the token from the request header
  • Check if it is present and valid
  • Return the player if the token is valid

Play games in the swagger

Conclusion

This tutorial guided you through the complete lifecycle of building a professional web service. You progressed from understanding the Client-Server principle to implementing a robust REST API:

  • Being client of an external API
  • Creating your own API and own endpoints
  • Using Pydantic models to enforce strict data contracts and prevent “garbage” data
  • Implementing Token-based authentication to protect sensitive routes.

End of the Lab

Important

When you have finished coding, don’t forget to:

    • If your service is terminated, all unpushed code is lost…
    • to free up reserved resources