API de base
Ce chapitre est le premier gros tutoriel pratique du wiki.
Il montre comment construire une API FastAPI de base en reprenant la même logique d'architecture que le repo de référence task-organizer-API, tout en ajoutant le guidage qui manque à un débutant.
Introduction
Ce chapitre sert de socle pour tout le reste du wiki. Au lieu d'empiler des snippets isolés, on construit dès le départ une petite API de tâches avec une structure proche d'un vrai projet FastAPI :
main.pypour assembler l'application,config.pypour la configuration,database/pour la persistance,services/pour la logique métier,api/pour l'interface HTTP.
L'objectif n'est donc pas seulement d'obtenir quatre endpoints CRUD. L'objectif est de comprendre pourquoi chaque morceau du code vit à cet endroit, afin que les chapitres suivants puissent réutiliser ce même socle sans repartir de zéro.
Ce que tu vas construire
À la fin de ce chapitre, tu auras un projet capable de :
- démarrer avec
uvicorn, - exposer une documentation automatique,
- créer une tâche,
- lire une tâche,
- mettre à jour son statut,
- supprimer une tâche,
- gérer proprement les cas simples de tâche introuvable.
Prérequis minimum
Ce chapitre convient si tu maîtrises déjà :
- les bases de Python,
- quelques commandes terminal simples,
- l'idée générale d'une requête HTTP et d'une réponse JSON,
- les notions les plus simples d'une base de données relationnelle.
Si certains mots sont encore flous, retiens simplement ceci avant de coder :
- endpoint : une route API appelée par un client.
- schema : la forme des données d'entrée ou de sortie.
- model : la structure persistée en base.
- session : l'objet qui porte les échanges avec la base.
- dependency injection : la manière dont FastAPI fournit automatiquement certains objets.
- service layer : l'endroit où l'on met la logique métier plutôt que dans les routes.
Vision d'ensemble
Pendant tout le chapitre, garde ce flux en tête :
- le client appelle une route HTTP ;
- la route valide l'entrée et récupère ses dépendances ;
- le service applique la logique métier ;
- la base lit ou écrit les données ;
- FastAPI renvoie une réponse JSON conforme au schéma attendu.
Ce fil conducteur est plus important que chaque détail de syntaxe : si tu comprends ce trajet, le reste du wiki sera beaucoup plus facile à suivre.
Architecture type retenue
Nous allons viser cette structure :
fastapi-base-api/
├── main.py
├── config.py
├── requirements.txt
├── .gitignore
├── .env
├── .env.example
├── api/
│ ├── __init__.py
│ ├── router.py
│ ├── dependencies.py
│ └── schemas/
│ ├── __init__.py
│ └── task.py
├── database/
│ ├── __init__.py
│ ├── models.py
│ └── session.py
└── services/
├── __init__.py
└── task.py
Repères utiles avant de lire le code
Avant de coder, garde ces repères simples en tête :
main.pyassemble l'application, mais ne doit pas contenir toute la logique métier.config.pylit la configuration, surtout la connexion base de données.database/models.pydécrit la forme des données en base.api/schemas/task.pydécrit la forme des données échangées par l'API.database/session.pyprépare le moteur et les sessions de travail.services/task.pycontient ce que l'application fait vraiment avec une tâche.api/dependencies.pydemande à FastAPI de fournir les bons objets automatiquement.api/router.pyreçoit les appels HTTP et les envoie au service.
Si tu comprends déjà ce découpage, le code sera beaucoup plus facile à lire.
Étape 1 — créer le projet et installer les dépendances
Commence par créer le dossier de travail, l'environnement virtuel et les dépendances.
mkdir fastapi-base-api
cd fastapi-base-api
python -m venv .venv
source .venv/bin/activate
Crée ensuite un fichier requirements.txt :
fastapi
uvicorn[standard]
sqlmodel
sqlalchemy
asyncpg
pydantic-settings
scalar-fastapi
Puis installe les dépendances :
pip install -r requirements.txt
Étape 2 — préparer les fichiers de base du projet
Crée maintenant les dossiers et fichiers clés.
mkdir -p api/schemas database services
touch main.py config.py .env .env.example .gitignore
touch api/__init__.py api/router.py api/dependencies.py
touch api/schemas/__init__.py api/schemas/task.py
touch database/__init__.py database/models.py database/session.py
touch services/__init__.py services/task.py
À ce stade, l'idée n'est pas encore de tout comprendre dans le détail. L'idée est de voir où le code va vivre.
Ajouter .gitignore
Même dans un mini-projet pédagogique, il faut éviter de versionner les secrets et les fichiers temporaires.
__pycache__/
*.py[cod]
.venv/
.env
.DS_Store
Ajouter .env.example
Ce fichier sert de modèle pour montrer quelles variables doivent exister sans exposer de vraies valeurs sensibles.
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=taskapi_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=change_me
Étape 3 — préparer PostgreSQL
Le repo de référence repose sur PostgreSQL. Pour rester fidèle à sa logique d'architecture, on garde cette base.
Option simple si PostgreSQL tourne déjà chez toi
Si tu as déjà un utilisateur postgres local :
createdb taskapi_db
Option plus explicite si tu veux tout créer proprement
createuser postgres --pwprompt
createdb taskapi_db -O postgres
Ensuite, remplis .env avec de vraies valeurs locales :
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=taskapi_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_real_password
Si PostgreSQL n'est pas lancé, ou si les identifiants ne correspondent pas à ton installation locale, l'API ne démarrera pas correctement.
Étape 4 — écrire config.py
Le rôle de ce fichier est de centraliser la configuration et de fabriquer l'URL de connexion à la base.
from pydantic_settings import BaseSettings, SettingsConfigDict
class DatabaseSettings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
model_config = SettingsConfigDict(
env_file=".env",
env_ignore_empty=True,
extra="ignore",
)
@property
def POSTGRES_URL(self) -> str:
return (
f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f"@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
)
settings = DatabaseSettings()
Pourquoi ce fichier existe ?
Parce que tu ne veux pas disperser les informations sensibles dans tout le projet. Tu veux un seul endroit qui lit l'environnement et fabrique proprement la connexion.
Le repo d'origine construisait déjà l'URL de connexion dans config.py. Ici, on garde cette idée, mais on ajoute explicitement le mot de passe pour rendre le setup plus complet et plus clair.
Étape 5 — définir le modèle de données dans database/models.py
Ici, on définit ce qu'est une tâche en base de données.
from enum import Enum
from sqlalchemy import Enum as SAEnum
from sqlmodel import Field, SQLModel
class Status_Task(str, Enum):
to_do = "to_do"
in_progress = "in_progress"
completed = "completed"
class Task(SQLModel, table=True):
__tablename__ = "task"
id: int | None = Field(default=None, primary_key=True)
title: str
description: str
assignee: str
status: Status_Task = Field(
sa_type=SAEnum(Status_Task, native_enum=False),
default=Status_Task.to_do,
)
Cette étape sert à définir le moule de tes données.
Ce qu'il faut remarquer
Taskreprésente la table SQL.idest la clé primaire.statusest limité à trois valeurs connues.native_enum=Falseévite certaines surprises selon les configurations SQL.
Étape 6 — définir les schémas d'entrée et de sortie dans api/schemas/task.py
Ici, on définit la forme des données API.
from pydantic import BaseModel, Field
from database.models import Status_Task
class BaseTask(BaseModel):
title: str
description: str
assignee: str
class TaskRead(BaseTask):
id: int
status: Status_Task
class TaskCreate(BaseTask):
pass
class TaskUpdate(BaseModel):
status: Status_Task | None = Field(default=None)
Pourquoi ne pas utiliser directement Task partout ?
Parce qu'une API n'a pas toujours la même forme en entrée qu'en sortie. Les schémas permettent de clarifier cela proprement.
TaskCreatesert à recevoir les données de création.TaskReadsert à renvoyer une tâche complète au client.TaskUpdatesert à mettre à jour seulement ce qui change.
Étape 7 — créer la couche de session base de données dans database/session.py
Ici, on prépare le moteur async, la session et la création des tables.
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlmodel import SQLModel
from config import settings
engine = create_async_engine(
url=settings.POSTGRES_URL,
echo=True,
)
async_session = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
async def create_db_tables():
async with engine.begin() as connection:
await connection.run_sync(SQLModel.metadata.create_all)
async def get_session():
async with async_session() as session:
yield session
Ce que fait ce fichier
engineouvre la grande connexion technique vers PostgreSQL.async_sessionfabrique des sessions de travail.create_db_tables()crée les tables au démarrage si elles n'existent pas.get_session()fournit une session temporaire à chaque requête.
Pourquoi expire_on_commit=False ?
Parce qu'après un commit(), on veut encore pouvoir manipuler proprement l'objet chargé sans déclencher des comportements de rechargement peu intuitifs pour un débutant.
Étape 8 — créer le service métier dans services/task.py
Dans cette architecture, le service porte la logique CRUD de la ressource Task.
from sqlalchemy.ext.asyncio import AsyncSession
from api.schemas.task import TaskCreate
from database.models import Task
class TaskService:
def __init__(self, session: AsyncSession):
self.session = session
async def get_task(self, id: int) -> Task | None:
return await self.session.get(Task, id)
async def add_task(self, task_create: TaskCreate) -> Task:
new_task = Task(**task_create.model_dump())
self.session.add(new_task)
await self.session.commit()
await self.session.refresh(new_task)
return new_task
async def update_task(self, id: int, task_update: dict) -> Task | None:
task = await self.get_task(id)
if task is None:
return None
task.sqlmodel_update(task_update)
self.session.add(task)
await self.session.commit()
await self.session.refresh(task)
return task
async def delete_task(self, id: int) -> bool:
task = await self.get_task(id)
if task is None:
return False
await self.session.delete(task)
await self.session.commit()
return True
Pourquoi cette couche est importante
On évite de mettre toute la logique directement dans les routes.
Les routes doivent rester simples :
- elles reçoivent la requête,
- elles délèguent au service,
- elles renvoient la réponse.
Le service, lui, fait le vrai travail métier.
Ce que cette version améliore déjà
Par rapport au repo de référence, on gère ici plus proprement deux cas :
- la mise à jour d'une tâche absente
- la suppression d'une tâche absente
Au lieu de laisser planter le code plus loin, le service renvoie None ou False, puis la route traduit cela en 404.
Étape 9 — préparer l'injection de dépendances dans api/dependencies.py
Ici, on demande à FastAPI de fournir automatiquement :
- une session de base de données,
- un
TaskServicedéjà prêt.
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from database.session import get_session
from services.task import TaskService
SessionDep = Annotated[AsyncSession, Depends(get_session)]
def get_task_service(session: SessionDep) -> TaskService:
return TaskService(session)
ServiceDep = Annotated[TaskService, Depends(get_task_service)]
Pourquoi cette étape est précieuse
C'est ici que FastAPI devient très agréable : tu n'as pas besoin d'aller chercher la session toi-même dans chaque route.
Tu déclares ce dont tu as besoin, et FastAPI te le fournit.
Étape 10 — écrire les routes CRUD dans api/router.py
Ici, on expose la ressource Task à travers HTTP.
from fastapi import APIRouter, HTTPException, status
from .dependencies import ServiceDep
from .schemas.task import TaskCreate, TaskRead, TaskUpdate
router = APIRouter(prefix="/task", tags=["Task"])
@router.get("/", response_model=TaskRead)
async def get(id: int, service: ServiceDep):
task = await service.get_task(id)
if task is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return task
@router.post("/", response_model=TaskRead, status_code=status.HTTP_201_CREATED)
async def create(task_create: TaskCreate, service: ServiceDep):
return await service.add_task(task_create)
@router.patch("/", response_model=TaskRead)
async def update(id: int, task_update: TaskUpdate, service: ServiceDep):
update_data = task_update.model_dump(exclude_unset=True)
task = await service.update_task(id, update_data)
if task is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return task
@router.delete("/")
async def delete(id: int, service: ServiceDep):
deleted = await service.delete_task(id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return {"message": f"Task with ID {id} has been deleted successfully"}
Ce qu'il faut remarquer ici
- les routes sont fines,
- elles ne contiennent pas toute la logique CRUD,
- elles traduisent les cas d'erreur en réponses HTTP,
- elles annoncent explicitement les schémas de réponse.
Pour rester proche du repo de référence, on garde ici un style de routes avec id en query parameter. Plus tard, tu pourras faire évoluer cette base vers une forme plus REST comme /task/{id}.
Étape 11 — assembler l'application dans main.py
Il ne reste plus qu'à relier les morceaux entre eux.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from scalar_fastapi import get_scalar_api_reference
from api.router import router
from database.session import create_db_tables
@asynccontextmanager
async def lifespan(app: FastAPI):
await create_db_tables()
yield
app = FastAPI(
title="Task Organizer API",
description="Une API de base construite avec FastAPI",
lifespan=lifespan,
)
app.include_router(router)
@app.get("/scalar", include_in_schema=False)
def get_scalar_docs():
return get_scalar_api_reference(
openapi_url=app.openapi_url,
title="Scalar API",
)
main.py joue le rôle de chef d'orchestre :
- il démarre l'application,
- il crée les tables au lancement,
- il branche les routes,
- il expose une documentation alternative via Scalar.
Étape 12 — lancer l'application
Depuis la racine du projet :
uvicorn main:app --reload
Si tout va bien, tu devrais pouvoir ouvrir :
http://127.0.0.1:8000/docshttp://127.0.0.1:8000/scalar
Vérification guidée
Test 1 — créer une tâche
curl -X POST http://127.0.0.1:8000/task/ -H "Content-Type: application/json" -d '{
"title": "Préparer le wiki FastAPI",
"description": "Construire la section API de base",
"assignee": "Enes"
}'
Réponse attendue :
{
"title": "Préparer le wiki FastAPI",
"description": "Construire la section API de base",
"assignee": "Enes",
"id": 1,
"status": "to_do"
}
Test 2 — lire la tâche créée
curl "http://127.0.0.1:8000/task/?id=1"
Test 3 — mettre à jour le statut
curl -X PATCH "http://127.0.0.1:8000/task/?id=1" -H "Content-Type: application/json" -d '{"status": "completed"}'
Réponse attendue :
{
"title": "Préparer le wiki FastAPI",
"description": "Construire la section API de base",
"assignee": "Enes",
"id": 1,
"status": "completed"
}
Test 4 — supprimer la tâche
curl -X DELETE "http://127.0.0.1:8000/task/?id=1"
Réponse attendue :
{
"message": "Task with ID 1 has been deleted successfully"
}
Test 5 — vérifier le 404
curl "http://127.0.0.1:8000/task/?id=999"
Réponse attendue :
{
"detail": "Task not found"
}
Test 6 — vérifier directement en base
Si tu veux vérifier que les données existent vraiment en base :
psql taskapi_db -c "SELECT id, title, status, assignee FROM task;"
Même si FastAPI te montre déjà la réponse JSON, vérifier aussi la base aide beaucoup à comprendre que ton API n'est pas juste un affichage : elle écrit réellement dans PostgreSQL.
Ce que ce chapitre améliore par rapport au repo de référence
Ce tutoriel reste fidèle à task-organizer-API, mais il le rend plus praticable pour un débutant.
Voici ce qu'il apporte en plus :
- un
requirements.txt - un
.gitignore - un
.env.example - une configuration PostgreSQL plus explicite
- une meilleure gestion des cas
404surPATCHetDELETE - une vérification guidée plus complète
- plus d'explications sur le rôle exact de chaque fichier
Correspondance avec le repo de référence
Le lien avec task-organizer-API est volontairement visible :
main.pyjoue le même rôle d'entrée principaleconfig.pycentralise la configuration DBapi/router.pyregroupe les endpointsapi/dependencies.pyprépare session et serviceapi/schemas/task.pyporte les schémas d'entrée/sortieservices/task.pyporte la logique CRUDdatabase/models.pydécrit la structureTaskdatabase/session.pygère moteur, session et tables
Donc le tutoriel reste fidèle à la même architecture, même si la version pédagogique est plus guidée et un peu plus robuste.
Pièges et erreurs fréquentes
- oublier d'activer le virtualenv avant de lancer
uvicorn - oublier de créer la base PostgreSQL avant de lancer l'API
- avoir un
.envincorrect - confondre
Task(modèle DB) etTaskCreate/TaskRead(schémas API) - mettre toute la logique directement dans les routes
- oublier
commit()ourefresh()après la création / mise à jour - oublier que
TaskUpdatene contient ici que le statut - penser que
main.pydoit contenir tout le projet
Bonnes pratiques à retenir
- garder les routes fines
- garder la logique métier dans
TaskService - nommer les schémas selon leur rôle (
Create,Read,Update) - expliquer chaque couche quand tu apprends une nouvelle architecture
- faire apparaître le flow complet requête → service → base → réponse
- préparer un
.env.exampledès qu'il y a de la configuration - versionner seulement ce qui doit l'être
Variantes et évolutions possibles
Cette API de base peut ensuite grandir vers :
- une authentification utilisateur,
- un système de login/logout,
- des relations SQL,
- une gestion d'erreurs plus riche,
- des middlewares,
- des tests automatisés,
- un packaging Docker.
C'est exactement ce qui ouvrira naturellement le second grand bloc du wiki.
Checklist de validation
Avant de considérer ce socle comme compris, vérifie que tu peux :
- expliquer où vit la logique métier
- montrer où FastAPI injecte la session et le service
- expliquer pourquoi on sépare modèle SQL et schéma API
- montrer où se fait la connexion PostgreSQL
- créer, lire, mettre à jour et supprimer une tâche
- relancer le projet et vérifier les endpoints principaux sans te perdre dans l’arborescence
Résumé final
Dans ce chapitre, tu as construit une API FastAPI de base avec une architecture claire inspirée du repo task-organizer-API.
Tu as vu :
- comment organiser le projet,
- comment brancher PostgreSQL,
- comment séparer schémas, modèles, services et routes,
- comment démarrer l'application,
- comment vérifier le fonctionnement avec des appels HTTP réels.
Si tu retiens une seule chose, retiens celle-ci :
une bonne API de base ne commence pas par une montagne de fonctionnalités ; elle commence par une structure lisible, un flow clair et du code bien réparti.
Pour aller plus loin
- Parcours de lecture — pour choisir le meilleur point d’entrée selon ton objectif
- FAQ, erreurs fréquentes et conseils pratiques — pour retrouver rapidement les pièges classiques du socle
- Advanced FastAPI — pour passer du socle vers une API plus réaliste
- Authentification avec FastAPI — pour commencer la montée en puissance par la gestion de l’identité