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!"
Web APIs & Data formats
Before you start
This tutorial combines explanations and code phases. The explanations should not take precedence over those of your teacher.
- 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.
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
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.
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
- Call the web service
- Check the response
- 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}.
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
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
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)
- to have this endpont:
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 serviceFinally, 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: boolMany 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:
- The Request: a client sends request to the endpoint with a JSON body:
{"username": "bob", "email": "bob@email.com", ...}. - 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?
- Conversion: If everything is correct, FastAPI automatically transforms that JSON into a Python object named
p. - 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:
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.
PlayerReadModel: Used as an output to display information about a Player.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
- the output must be a list of
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:
PlayerCreatevsPlayerUpdate: Currently,PlayerModelis 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.
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
When you have finished coding, don’t forget to:
-
- If your service is terminated, all unpushed code is lost…
-
- to free up reserved resources