Une API REST complète, pédagogique et francophone, écrite en Go avec PostgreSQL,
sans le moindre ORM : uniquement la bibliothèque standarddatabase/sql(via le pilote
pgx) et des requêtes SQL écrites à la main. Chaque choix technique est expliqué et justifié
pour que vous appreniez en lisant le code et la documentation.
Domaine métier volontairement simple et parlant : une bibliothèque qui gère des livres,
des auteurs, des catégories, des membres et des emprunts. Ce support couvre l'ensemble des
briques d'une vraie API de production : authentification JWT, autorisation par rôle, pagination,
transactions, procédures stockées, triggers, tâches planifiées, observabilité, sécurité et
performances — le tout en exploitant les fonctionnalités avancées de PostgreSQL.
Dépôt jumeau (MariaDB). Ce projet est le jumeau PostgreSQL d'un dépôt MariaDB. Les deux
partagent une parité fonctionnelle stricte : mêmes endpoints, mêmes payloads JSON, mêmes
codes HTTP, même logique métier. Seule la couche base de données diffère (pilotepgx,
placeholders$1/$2, PL/pgSQL,pg_cron, types natifsENUM/DOMAIN/JSONB/NUMERIC,
extensions, index avancés,ILIKE,RETURNING…). Vous pouvez comparer les deux dépôts pour
voir concrètement ce qui change — et ce qui ne change pas — d'un moteur à l'autre.
- Pourquoi « sans ORM » ?
- Objectifs pédagogiques
- Prérequis
- Démarrage rapide
- Architecture en couches
- Arborescence du dépôt
- Configuration (
.env) - Commandes utiles
- Aperçu de l'API
- PostgreSQL en action
- Comptes de démonstration
- Tests
- Observabilité
- Documentation détaillée
- FAQ
- Erreurs fréquentes et dépannage
- Bonnes pratiques illustrées
- Ressources complémentaires
- Licence
Un ORM (Object-Relational Mapper, comme GORM en Go) traduit automatiquement vos structures
en tables et génère le SQL à votre place. C'est confortable, mais cela cache ce qui se passe
réellement. Ce projet fait le choix inverse, assumé et pédagogique :
| Sans ORM (ce projet) | Avec ORM |
|---|---|
| Vous voyez et maîtrisez chaque requête SQL | Le SQL est généré, parfois imprévisible |
| Vous comprenez requêtes préparées, transactions, index | Ces notions sont masquées derrière l'abstraction |
| Zéro magie : le code fait exactement ce qu'il dit | « Effets de bord » difficiles à diagnostiquer |
| Dépendances minimales, surface d'attaque réduite | Grosse dépendance transverse |
| Idéal pour apprendre les fondamentaux | Idéal pour aller vite une fois les bases acquises |
On n'utilise donc que :
database/sql— la bibliothèque standard de Go pour dialoguer avec une base SQL ;pgx(jackc/pgx/v5) — le pilote (driver) PostgreSQL le plus complet et le plus performant de l'écosystème Go. On l'utilise ici en modestdlib: il s'expose comme un pilotedatabase/sqlclassique (on garde*sql.DB,*sql.Tx…). Ce n'est pas un ORM : il transporte juste les requêtes que vous écrivez.
Résultat : en lisant ce dépôt, vous apprenez le SQL (et le dialecte PostgreSQL en particulier),
la sécurité et l'architecture d'une API, pas les conventions d'un framework particulier.
En explorant ce projet, vous apprendrez à :
- Structurer une API en couches (handlers → services → repositories) avec injection de dépendances manuelle, sans aucune variable globale.
- Écrire du SQL sûr : requêtes préparées paramétrées (
$1, $2…), liste blanche pour le tri, traduction des erreurs SQL (codesSQLSTATE) en erreurs métier. - Sécuriser une API : JWT + refresh tokens avec rotation, bcrypt, rate limiting, en-têtes de
sécurité, timeouts, protection anti Mass-Assignment, rôle base au moindre privilège, exécution
non-
rootdans le conteneur. - Exploiter les fonctionnalités avancées de PostgreSQL : extensions (
pgcrypto,pg_trgm,uuid-ossp,pg_cron), types natifs (ENUM,DOMAIN,JSONB,NUMERIC,uuid,timestamptz), fonctions et procédures PL/pgSQL (INOUT), triggers (BEFORE/AFTER/INSTEAD OF), vues et vue matérialisée, tâchespg_cron, contraintesCHECK, et une riche palette d'index (B-tree, GIN + trigrammes, GIN + JSONB, BRIN, partiels, couvrants). - Gérer les transactions de deux façons : dans une procédure stockée (emprunt) et dans
une transaction Go (retour de livre, avec verrou
FOR UPDATE). - Rendre l'application observable : logs structurés (
slog), métriques Prometheus, sondes de liveness/readiness. - Industrialiser avec Docker (multi-stage build, image PostgreSQL étendue avec
pg_cron), Docker Compose, unMakefileet des scripts d'exploitation (démarrage, sauvegarde, restauration…).
Le chemin recommandé (et le plus simple) n'exige que Docker :
- Docker Engine récent et Docker Compose v2 (commande
docker compose). L'ancienne commandedocker-compose(v1) reste supportée par les scripts.
Pour développer/compiler hors Docker (facultatif) :
- Go 1.25+ (grâce aux toolchains Go, la bonne version est téléchargée automatiquement).
- Un serveur PostgreSQL 18 accessible (avec l'extension
pg_croninstallée et préchargée), ou simplement le conteneur PostgreSQL de ce dépôt. make(facultatif, pour les raccourcis duMakefile).
Aucune connaissance préalable d'un framework web n'est nécessaire : on s'appuie sur la
bibliothèque standard net/http (routeur ServeMux de Go 1.22+).
Trois commandes suffisent pour tout lancer.
cp .env.example .envOuvrez .env et remplacez au minimum les secrets par de vraies valeurs :
BDD_MOT_DE_PASSE(mot de passe du rôle applicatifapp_bibliotheque) ;BDD_ROOT_MOT_DE_PASSE(mot de passe du superutilisateurpostgres) ;JWT_SECRET(au moins 32 caractères ; générez-en un avecopenssl rand -base64 48).
L'application refuse de démarrer si
BDD_MOT_DE_PASSEouJWT_SECRETsont vides, ou si le
secret JWT fait moins de 32 caractères. C'est volontaire : « échouer tôt et bruyamment » vaut
mieux qu'un démarrage silencieux dans un état incorrect.
docker compose up -d --buildCette commande construit l'image PostgreSQL 18 étendue avec pg_cron et l'image de l'API. Au
tout premier démarrage, PostgreSQL exécute dans l'ordre tous les scripts sql/ (extensions,
rôle, types, tables, index, fonctions, vues, procédures, triggers, tâches pg_cron, jeu de
données de démonstration), puis applique le secret du rôle applicatif. L'API ne démarre
qu'après que la base est signalée « saine » (healthcheck via pg_isready).
Message bénin au 1er démarrage. Un log
FATAL: database "bibliotheque" does not existpeut
apparaître une fois dans les journaux de PostgreSQL :pg_crontente de se connecter pendant
l'amorçage, avant que la base ne soit créée. C'est normal et sans conséquence — il se
reconnecte ensuite. Voir Erreurs fréquentes et dépannage.
curl http://localhost:8080/healthRéponse attendue :
{"succes":true,"donnees":{"statut":"ok","version":"1.0.0","duree_fonctionnement":"12.3s"}}curl -s -X POST http://localhost:8080/api/v1/auth/connexion \
-H "Content-Type: application/json" \
-d '{"email":"admin@bibliotheque.fr","mot_de_passe":"MotDePasse123!"}'Vous recevez le profil de l'utilisateur et une paire de jetons (jeton_acces +
jeton_rafraichissement). Placez le jeton d'accès dans l'en-tête Authorization: Bearer <jeton>
pour appeler les routes protégées.
Astuce : pour enchaîner les appels, stockez le jeton dans une variable shell (voir
Aperçu de l'API).
L'application suit une architecture en couches stricte. Chaque couche a une seule
responsabilité et ne parle qu'à la couche immédiatement inférieure. Les dépendances sont
injectées « à la main » depuis cmd/api/main.go (le composition root).
┌──────────────────────────────────────────────────┐
Client (curl, │ Middlewares globaux (pile « oignon ») │
navigateur, ─► │ ID requête → Logs → Recovery → En-têtes │
Postman…) │ sécurité → CORS → Rate limit → Limite corps │
│ → Timeout → Métriques │
└───────────────────────┬──────────────────────────┘
│
┌───────────────────────▼──────────────────────────┐
│ HANDLERS (couche HTTP) │
│ Décodent le JSON, appellent le service, │
│ écrivent la réponse enveloppée. Aucun SQL. │
└───────────────────────┬──────────────────────────┘
│
┌───────────────────────▼──────────────────────────┐
│ SERVICES (logique métier) │
│ Valident, orchestrent, appliquent les règles │
│ (quotas, autorisations). Aucun SQL, aucun HTTP. │
└───────────────────────┬──────────────────────────┘
│
┌───────────────────────▼──────────────────────────┐
│ REPOSITORIES (accès aux données) │
│ SEULE couche qui écrit du SQL. Requêtes │
│ préparées ($1,$2), transactions, CALL de │
│ procédures, RETURNING… │
└───────────────────────┬──────────────────────────┘
│ pilote pgx (database/sql)
┌───────────────────────▼──────────────────────────┐
│ PostgreSQL 18 : tables, types (ENUM/DOMAIN/ │
│ JSONB), vues + vue matérialisée, fonctions et │
│ procédures PL/pgSQL, triggers, index avancés, │
│ extensions et tâches pg_cron │
└──────────────────────────────────────────────────┘
Pourquoi séparer ainsi ?
- Testabilité : chaque service dépend d'une interface de repository (voir
internal/service/interfaces.go), pas d'une implémentation concrète. On peut donc injecter un faux repository dans les tests, sans base de données. - Sécurité : le SQL est centralisé dans les repositories, donc facile à auditer.
- Clarté : on sait toujours où chercher (une règle métier → un service ; une requête → un repository).
.
├── cmd/api/main.go # Point d'entrée : assemblage (DI) + arrêt gracieux
├── internal/ # Code privé de l'application (non importable de l'extérieur)
│ ├── apperreur/ # Type d'erreur applicative + codes métier ↔ statuts HTTP
│ ├── auth/ # JWT (accès) + refresh tokens + bcrypt (mots de passe)
│ ├── config/ # Lecture de la config depuis l'environnement (12-Factor)
│ ├── contexte/ # Valeurs transportées dans context.Context (ID requête, user)
│ ├── database/ # Ouverture du pool pgx, transactions, mapping des erreurs SQLSTATE
│ ├── handler/ # Couche HTTP : handlers + définition des routes
│ ├── middleware/ # Intercepteurs : logs, sécurité, rate limit, auth, timeout…
│ ├── models/ # Entités (sortie) et structures d'entrée (DTO anti Mass-Assignment)
│ ├── observabilite/ # Journalisation structurée (slog) + métriques Prometheus
│ ├── reponse/ # Enveloppe JSON homogène (succès / erreur)
│ ├── repository/ # Accès aux données : TOUT le SQL vit ici (placeholders $N)
│ ├── scheduler/ # Ordonnanceur de tâches périodiques CÔTÉ APP (goroutines)
│ ├── service/ # Logique métier + interfaces de repository
│ └── validation/ # Validation des entrées écrite à la main (sans lib externe)
├── sql/ # Tout le SQL, découpé par nature et numéroté pour l'ordre d'exécution
│ ├── extensions/ # 00 extensions (pgcrypto, pg_trgm, uuid-ossp)
│ ├── schema/ # 01 rôle & privilèges, 02 types (ENUM/DOMAIN), 03 tables, 04 index
│ ├── functions/ # 05 fonctions PL/pgSQL
│ ├── views/ # 06 vues + vue MATÉRIALISÉE (rank, FILTER)
│ ├── procedures/ # 07 procédures PL/pgSQL (paramètres INOUT, transactions)
│ ├── triggers/ # 08 triggers (BEFORE/AFTER/INSTEAD OF) + fonctions trigger
│ ├── cron/ # 09 tâches pg_cron (retards, purge, archivage, stats, VACUUM…)
│ ├── data/ # 10 jeu de données de démonstration (seed)
│ ├── demos/ # Scripts pédagogiques autonomes (types, index…) à lancer avec psql
│ └── migrations/ # Exemples de migrations versionnées (évolution de schéma)
├── docker/postgres/ # Image PostgreSQL 18 + pg_cron (Dockerfile) et script d'init du secret
├── scripts/ # Scripts d'exploitation (start, stop, reset, backup, restore…)
├── tests/integration/ # Emplacement des tests d'intégration (tag « integration »)
├── docker-compose.yml # Orchestration API + PostgreSQL
├── Dockerfile # Build multi-stage de l'API (image finale ~20 Mo, non-root)
├── Makefile # Raccourcis (compiler, tester, démarrer…)
├── .env.example # Modèle de configuration à copier en .env
├── README.md # Ce fichier
├── API.md # Référence complète des endpoints (+ exemples curl)
├── DATABASE.md # Schéma, types, index, vues, fonctions, procédures, triggers, pg_cron
├── DOCKER.md # Cycle de vie Docker (toutes les commandes expliquées)
├── POSTGRESQL.md # Le moteur en profondeur : psql, rôles, extensions, MVCC, VACUUM, EXPLAIN…
├── openapi.yaml # Spécification OpenAPI 3.0.3 de l'API
└── docs/
├── SECURITE.md # Menaces et protections mises en place, expliquées
└── PERFORMANCES.md # Pool pgx, prepared statements, contextes, index, VACUUM/ANALYZE…
Toute la configuration provient de variables d'environnement (méthodologie 12-Factor),
jamais du code. Le fichier .env (copié depuis .env.example) sert à deux choses :
- Docker Compose y lit les variables pour substituer les
${VARIABLE}dedocker-compose.yml(mots de passe, ports exposés sur l'hôte, secret JWT…). - L'application (
internal/config/config.go) lit les variables d'environnement injectées. Hors Docker (make executer), elle lit aussi directement le fichier.env.
À retenir sur les ports : dans Docker,
SERVEUR_PORT_HOTEetBDD_PORT_HOTEdéfinissent le
port exposé sur votre machine, tandis que l'API écoute toujours sur8080dans le
conteneur et joint PostgreSQL via le nom de servicepostgres:5432(DNS interne Docker).
| Variable | Défaut | Obligatoire | Rôle |
|---|---|---|---|
APP_ENVIRONNEMENT |
developpement |
developpement ou production (durcit CORS, messages…) |
|
SERVEUR_ADRESSE |
0.0.0.0 |
Interface d'écoute HTTP | |
SERVEUR_PORT |
8080 |
Port d'écoute dans le processus | |
SERVEUR_DELAI_LECTURE |
10s |
Délai max de lecture requête (anti-Slowloris) | |
SERVEUR_DELAI_ECRITURE |
15s |
Délai max d'écriture de la réponse | |
SERVEUR_DELAI_INACTIF |
60s |
Délai keep-alive entre deux requêtes | |
SERVEUR_DELAI_TRAITEMENT |
10s |
Délai max de traitement applicatif (contexte annulé au-delà) | |
SERVEUR_DELAI_ARRET |
15s |
Fenêtre laissée aux requêtes en cours à l'arrêt | |
BDD_HOTE |
127.0.0.1 |
Hôte PostgreSQL (postgres dans Docker) |
|
BDD_PORT |
5432 |
Port PostgreSQL | |
BDD_NOM |
bibliotheque |
Nom de la base | |
BDD_UTILISATEUR |
app_bibliotheque |
Rôle applicatif (droits limités, non superutilisateur) | |
BDD_MOT_DE_PASSE |
(vide) | oui | Mot de passe du rôle applicatif |
BDD_MAX_CONNEXIONS_OUVERTES |
25 |
Taille max du pool de connexions | |
BDD_MAX_CONNEXIONS_INACTIVES |
25 |
Connexions gardées au repos | |
BDD_DUREE_VIE_CONNEXION |
5m |
Durée de vie max d'une connexion (recyclage) | |
BDD_DELAI_CONNEXION |
5s |
Délai max pour établir/valider la connexion | |
JWT_SECRET |
(vide) | oui | Clé HMAC-SHA256 (≥ 32 caractères) |
JWT_EMETTEUR |
api-bibliotheque |
Champ iss du jeton |
|
JWT_DUREE_ACCES |
15m |
Durée de vie d'un jeton d'accès (court) | |
JWT_DUREE_RAFRAICHISSEMENT |
168h |
Durée de vie d'un refresh token (7 jours) | |
CORS_ORIGINES_AUTORISEES |
* |
Origines CORS (* interdit en production) |
|
REQUETE_TAILLE_MAX_OCTETS |
1048576 (1 Mio) |
Taille max du corps d'une requête (anti-DoS) | |
RATE_LIMIT_PAR_SECONDE |
10 |
Requêtes/seconde autorisées par IP | |
RATE_LIMIT_RAFALE |
20 |
Pic de requêtes toléré (burst) | |
LOG_NIVEAU |
info |
debug | info | warn | error |
|
LOG_FORMAT |
texte |
json (prod) | texte (dev) |
| Variable | Défaut | Rôle |
|---|---|---|
COMPOSE_PROJECT_NAME |
bibliotheque |
Préfixe des conteneurs, réseaux et volumes |
SERVEUR_PORT_HOTE |
8080 |
Port de l'API exposé sur l'hôte |
BDD_PORT_HOTE |
5432 |
Port PostgreSQL exposé sur l'hôte (client psql externe) |
BDD_ROOT_MOT_DE_PASSE |
(à changer) | Mot de passe du superutilisateur postgres (administration) |
Tapez make (ou make aide) pour afficher toutes les cibles. Les principales :
| Commande | Effet |
|---|---|
make demarrer |
docker compose up -d --build (démarre API + PostgreSQL) |
make arreter |
docker compose down (arrête, conserve les données) |
make reconstruire |
Reconstruit les images et recrée les conteneurs |
make journaux |
Suit les logs de l'API (docker compose logs -f api) |
make nettoyer |
docker compose down -v (supprime les données du volume) |
make compiler |
Compile le binaire dans ./bin |
make executer |
Lance l'API en local (go run, nécessite un .env) |
make tester |
Lance tous les tests avec le détecteur de data races |
make tester-court |
Tests rapides uniquement (-short, sans intégration) |
make couverture |
Rapport de couverture HTML (coverage.html) |
make lint |
golangci-lint (analyse statique, doit être installé) |
make verifier |
gofmt + go vet + go build (contrôle rapide avant commit) |
Ces scripts détectent automatiquement docker compose (v2) ou docker-compose (v1) et lisent
votre .env. Ils demandent confirmation avant toute opération destructrice.
| Script | Rôle |
|---|---|
./scripts/start.sh |
Démarre la pile (construit les images au besoin) |
./scripts/stop.sh |
Arrête la pile, conserve le volume de données |
./scripts/reset.sh |
Réinitialise la base (supprime le volume, recharge schéma + seed) |
./scripts/rebuild.sh |
Reconstruit l'image de l'API sans cache, conserve les données |
./scripts/backup.sh |
Sauvegarde compressée (backups/….sql.gz) via pg_dump |
./scripts/restore.sh <fichier> |
Restaure la base depuis une sauvegarde (écrase l'existant) |
./scripts/clean.sh |
Nettoyage complet : conteneurs, volumes, images locales, réseau |
Exemple de cycle de sauvegarde / restauration :
./scripts/backup.sh
# → backups/bibliotheque_20260706_120000.sql.gz
./scripts/restore.sh backups/bibliotheque_20260706_120000.sql.gzLe détail de chaque commande Docker (build de l'image PostgreSQL +
pg_cron,psql,
pg_dump, sauvegarde de volume,prune…) et son équivalentdocker-composev1 est dans
DOCKER.md.
- Base des routes :
http://localhost:8080/api/v1 - Format d'échange : JSON (UTF-8), enveloppé de façon homogène.
- Identifiants publics : des UUID (le champ JSON s'appelle
id). Les clés internes (GENERATED ALWAYS AS IDENTITY) ne sont jamais exposées (protection contre l'énumération / IDOR). - Authentification :
Authorization: Bearer <jeton_acces>(JWT).
Succès (avec pagination optionnelle) :
{
"succes": true,
"donnees": { "...": "..." },
"meta": { "page": 1, "taille_par_page": 20, "total_elements": 28, "total_pages": 2 }
}Erreur :
{
"succes": false,
"erreur": {
"code": "VALIDATION",
"message": "Un ou plusieurs champs sont invalides.",
"details": { "email": "adresse e-mail invalide" }
}
}Les listes acceptent ces paramètres de requête :
| Paramètre | Exemple | Description |
|---|---|---|
page |
?page=2 |
Numéro de page (défaut 1) |
taille |
?taille=50 |
Éléments par page (défaut 20, max 100) |
tri |
?tri=titre |
Champ de tri (liste blanche par ressource, anti-injection) |
ordre |
?ordre=desc |
asc (défaut) ou desc |
recherche |
?recherche=hugo |
Recherche insensible à la casse (ILIKE, accélérée par index GIN) |
| filtres | ?disponible=true |
Filtres additionnels propres à chaque ressource |
Se connecter et mémoriser le jeton (avec jq) :
JETON=$(curl -s -X POST http://localhost:8080/api/v1/auth/connexion \
-H "Content-Type: application/json" \
-d '{"email":"chloe.durand@exemple.fr","mot_de_passe":"MotDePasse123!"}' \
| jq -r '.donnees.jetons.jeton_acces')Lister les livres disponibles, triés par prix décroissant (public) :
curl -s "http://localhost:8080/api/v1/livres?disponible=true&tri=prix&ordre=desc&taille=5" | jqEmprunter un livre (authentifié — remplacez l'UUID par un id de livre réel) :
curl -s -X POST http://localhost:8080/api/v1/emprunts \
-H "Authorization: Bearer $JETON" \
-H "Content-Type: application/json" \
-d '{"livre_id":"<UUID_DU_LIVRE>","duree_jours":21}' | jqConsulter mes emprunts et mes statistiques :
curl -s -H "Authorization: Bearer $JETON" http://localhost:8080/api/v1/moi/emprunts | jq
curl -s -H "Authorization: Bearer $JETON" http://localhost:8080/api/v1/moi/statistiques | jqLa référence exhaustive de tous les endpoints (méthode, rôle requis, paramètres, corps
attendu/retourné, codes HTTP, erreurs possibles, curl complet) est dans API.md,
et la spécification machine dans openapi.yaml.
Ce dépôt n'utilise pas PostgreSQL comme un simple entrepôt de lignes : il en exploite les
fonctionnalités distinctives. Chaque script sql/ est abondamment commenté ; la
documentation base de données et le guide PostgreSQL les
détaillent. Aperçu :
-
Extensions (
sql/extensions/00_extensions.sql) :pgcrypto(UUID aléatoiresgen_random_uuid()),pg_trgm(recherche floue par trigrammes),uuid-ossp, etpg_cron(planification). Activer un mécanisme entier d'une seule commande est une force majeure de PostgreSQL. -
Types natifs (
sql/schema/02_types.sql) : desENUMréutilisables (role_utilisateur,statut_emprunt), desDOMAINvalidés par regex (courriel,isbn13— la règle est définie une fois et appliquée partout), duuuid, dutimestamptz(UTC), dunumeric(8,2)(monnaie exacte) et dujsonb(journal d'audit indexable). -
Recherche plein texte / floue : la recherche du catalogue utilise
titre ILIKE '%terme%', rendue rapide par un index GIN + trigrammes (idx_livres_titre_trgm) — ce qu'un index B-tree ne peut pas optimiser à cause du joker en tête. -
JSONB : le
journal_auditstocke une phototo_jsonb(...)des lignes avant/après, indexée en GIN (WHERE nouvelles_valeurs @> '{"role":"admin"}'). -
Fonctions & procédures PL/pgSQL : calcul de pénalité, disponibilité, quota ; et surtout la procédure
pr_emprunter_livre(paramètresINOUT, verrouFOR UPDATE, codes de retour) appelée depuis Go par un simpleCALL pr_emprunter_livre($1, $2, $3, NULL, NULL, NULL). -
Triggers (
BEFORE/AFTER/INSTEAD OF) : normalisation (e-mail, ISBN), horodatage automatiquemodifie_le, audit JSONB générique, règles métier (RAISE EXCEPTION), et une vue rendue modifiable par un triggerINSTEAD OF. -
Vue matérialisée
vue_statistiques_livres: popularité des livres viarank() OVER (...)etCOUNT(...) FILTER (WHERE ...), rafraîchieCONCURRENTLYparpg_cron. -
pg_cron(sql/cron/09_cron.sql) : 7 tâches planifiées dans la base (PostgreSQL n'a pas d'ordonnanceur intégré, contrairement à l'Event Scheduler de MariaDB) :Tâche Planification Rôle bib_marquer_retards0 1 * * *Passe les emprunts échus en en_retardbib_purger_jetons0 * * * *Supprime les refresh tokens expirés/révoqués bib_archiver30 2 * * *Archive les emprunts rendus depuis > 1 an bib_statistiques0 3 * * *Calcule les statistiques quotidiennes (UPSERT) bib_refresh_stats15 3 * * *REFRESH MATERIALIZED VIEW CONCURRENTLYbib_nettoyer_audit0 4 * * 0Purge le journal d'audit (rétention 90 jours) bib_maintenance0 5 * * 0VACUUM ANALYZEhebdomadaire -
Transactions, de deux façons complémentaires : dans une procédure (
pr_emprunter_livre, atomicité côté base) et dans une transaction Go (Rendre, verrouFOR UPDATE+COMMIT/ROLLBACKautomatique — voirinternal/database/transaction.go).
Envie de manipuler ? Le dossier
sql/demos/contient des scripts autonomes et
pédagogiques (par ex.01_types.sql:UUID,JSONvsJSONB,ARRAY,ENUM,DOMAIN,
BYTEA,INTERVAL,NUMERICvsfloat…). Ils créent leurs propres objets préfixésdemo_et
les suppriment à la fin (aucune donnée du seed n'est touchée) :docker exec -i bibliotheque_postgres \ psql -U postgres -d bibliotheque -v ON_ERROR_STOP=1 -f - < sql/demos/01_types.sql
Le jeu de données (sql/data/10_seed.sql) crée ces comptes. Mot de passe commun :
MotDePasse123!
| Rôle | Remarque | |
|---|---|---|
admin@bibliotheque.fr |
admin |
Accès complet |
bibliothecaire@bibliotheque.fr |
bibliothecaire |
Gère catalogue et emprunts |
chloe.durand@exemple.fr |
membre |
Emprunts en cours et rendus |
david.petit@exemple.fr |
membre |
A un emprunt en retard |
emma.roux@exemple.fr |
membre |
Emprunts variés |
farid.benali@exemple.fr |
membre |
Emprunts en cours |
gwen.leroy@exemple.fr |
membre |
Emprunt sans date de retour (calculée) |
hugo.moreau@exemple.fr |
membre |
Compte inactif (illustre le refus de connexion) |
Le catalogue est peuplé de 28 livres, 12 auteurs et 8 catégories, avec des emprunts
dans divers états (en_cours, rendu, en_retard) pour illustrer immédiatement les vues et les
statistiques.
# Tous les tests, avec détection des accès concurrents (data races)
go test -race -count=1 ./... # ou : make tester
# Uniquement les tests rapides (unitaires), sans intégration
go test -short -count=1 ./... # ou : make tester-court
# Rapport de couverture HTML
make couverture # → coverage.htmlTests unitaires vs tests d'intégration. Les services dépendent d'interfaces de repository,
donc les tests unitaires peuvent injecter un faux repository et tourner sans base de données.
Les tests d'intégration (qui ont besoin d'un vrai PostgreSQL) se placent dans tests/integration/
et se protègent par la convention -short / un build tag dédié :
//go:build integration
package integration
// … tests nécessitant une base réelle …On les exécute alors explicitement :
go test -tags=integration ./tests/integration/...Trois points d'entrée non authentifiés :
| Endpoint | Rôle |
|---|---|
/health |
Liveness : « le processus est-il vivant ? ». Répond toujours 200 si vivant. |
/ready |
Readiness : « peut-il servir ? ». Vérifie PostgreSQL (503 si injoignable). |
/metrics |
Métriques Prometheus (compteurs, histogrammes de latence, runtime Go). |
Distinguer liveness et readiness évite les redémarrages inutiles : un orchestrateur
redémarre le conteneur sur échec de /health, mais se contente de retirer l'instance du service
sur échec de /ready (le temps que la base se rétablisse).
Les logs sont structurés (slog), au format json (production) ou texte (développement).
Chaque requête est tracée avec un identifiant de requête (X-Request-ID) pour corréler tous
ses logs. Aucune donnée sensible n'est journalisée (ni corps, ni query string, ni secret).
Exemple d'extraction d'une métrique :
curl -s http://localhost:8080/metrics | grep bibliotheque_http_requetes_total| Document | Contenu |
|---|---|
| API.md | Référence de tous les endpoints, par ressource, avec exemples curl complets |
| openapi.yaml | Spécification OpenAPI 3.0.3 (importable dans Swagger UI, Postman, Insomnia) |
| DATABASE.md | Schéma complet, types, index, vues, fonctions, procédures, triggers, pg_cron, transactions |
| POSTGRESQL.md | Le moteur en profondeur : comparatif MariaDB↔PostgreSQL, psql, rôles, extensions, MVCC, VACUUM, EXPLAIN, planification |
| DOCKER.md | Cycle de vie Docker : toutes les commandes, expliquées, v2 et v1 |
| docs/SECURITE.md | Chaque menace (injection SQL, XSS, CSRF, brute force, DoS…) et sa parade |
| docs/PERFORMANCES.md | Pool pgx, prepared statements, context, timeouts, index, VACUUM/ANALYZE |
Pourquoi le champ id est-il un UUID et pas un nombre ?
Exposer une clé séquentielle (1, 2, 3…) permettrait à un attaquant de deviner et d'énumérer
les ressources (faille IDOR). On expose donc un UUID non devinable (gen_random_uuid()) ; la
clé numérique GENERATED ALWAYS AS IDENTITY reste interne à la base.
Pourquoi deux systèmes de tâches planifiées (pg_cron et ordonnanceur Go) ?
pg_cron est idéal pour la maintenance des données (purge, archivage, agrégats, VACUUM), au
plus près des tables et sans dépendre de l'application. L'ordonnanceur Go
(internal/scheduler/) convient aux tâches applicatives (rafraîchir un cache, journaliser des
métriques internes…). La comparaison Event Scheduler / pg_cron / cron Linux / Kubernetes CronJob
est détaillée dans POSTGRESQL.md.
Qu'est-ce que pg_cron et pourquoi une image PostgreSQL sur mesure ?
PostgreSQL n'embarque pas d'ordonnanceur (contrairement à l'Event Scheduler de MariaDB).
pg_cron est l'extension standard pour planifier du SQL dans la base. Elle doit être préchargée
au démarrage (shared_preload_libraries), d'où l'image construite depuis docker/postgres/Dockerfile
(FROM postgres:18 + le paquet postgresql-18-cron).
Puis-je changer le mot de passe de démonstration ?
Oui : le seed contient un haché bcrypt de MotDePasse123!. Créez un nouveau compte via
/api/v1/auth/inscription, ou modifiez le seed et réinitialisez la base (./scripts/reset.sh).
L'API fonctionne-t-elle sans Docker ?
Oui : lancez un PostgreSQL 18 (avec pg_cron préchargé), renseignez .env (dont BDD_HOTE), puis
make executer. Il faudra charger vous-même les scripts sql/ dans l'ordre (voir
DATABASE.md et POSTGRESQL.md).
Où est écrit le SQL ?
Exclusivement dans internal/repository/. Les handlers et services n'en contiennent jamais.
Comment un emprunt gère-t-il la concurrence sur le dernier exemplaire ?
La procédure pr_emprunter_livre verrouille la ligne du livre (SELECT … FOR UPDATE) : deux
emprunts simultanés du dernier exemplaire sont sérialisés, le second voit 0 disponible.
| Symptôme | Cause probable et solution |
|---|---|
FATAL: database "bibliotheque" does not exist au 1er démarrage |
Bénin. pg_cron tente de se connecter pendant l'amorçage, avant la création de la base. Le message n'apparaît qu'une fois ; le serveur poursuit normalement. Rien à faire. |
configuration invalide : JWT_SECRET doit faire au moins 32… |
Secret JWT trop court. Générez-en un : openssl rand -base64 48. |
configuration invalide : BDD_MOT_DE_PASSE est obligatoire |
.env manquant ou variable vide. cp .env.example .env et renseignez les secrets. |
| L'API redémarre en boucle au premier lancement | Elle attend PostgreSQL « healthy ». Patientez (~40 s au 1er boot, le temps que tous les scripts sql/ s'exécutent), puis docker compose logs api. |
pg_cron ne planifie rien / extension absente |
pg_cron doit être préchargée (shared_preload_libraries=pg_cron) et cron.database_name=bibliotheque — c'est fait par le command: du docker-compose.yml. Vérifiez : SELECT * FROM cron.job;. |
password authentication failed for user "app_bibliotheque" |
Le secret du rôle applicatif diffère entre l'API et la base. Alignez BDD_MOT_DE_PASSE puis réinitialisez la base (./scripts/reset.sh) pour rejouer le script d'init du mot de passe. |
401 « Jeton d'accès invalide ou expiré » |
Le jeton d'accès dure 15 min. Rafraîchissez-le via /api/v1/auth/rafraichir. |
429 « Trop de requêtes » |
Limite de débit atteinte (10 req/s par IP par défaut). Patientez ou ajustez RATE_LIMIT_*. |
403 « Vous n'avez pas les droits nécessaires » |
Rôle insuffisant. Connectez-vous avec un compte admin ou bibliothecaire selon la route. |
| Les modifications du code Go ne sont pas prises en compte | Reconstruisez l'image : ./scripts/rebuild.sh (ou make reconstruire). |
| Le port 8080 (ou 5432) est déjà utilisé | Changez SERVEUR_PORT_HOTE (ou BDD_PORT_HOTE) dans .env. |
Ce dépôt met concrètement en œuvre, avec explications dans le code :
- 12-Factor App : configuration par l'environnement, secrets hors du code.
- Injection de dépendances manuelle, aucune variable globale mutable.
- Requêtes préparées paramétrées systématiques (
$1, $2…) ; liste blanche pour l'ORDER BY. - Défense en profondeur côté base : types
DOMAIN(regex) et contraintesCHECK, rôle au moindre privilège (ALTER DEFAULT PRIVILEGES, pas deDROP/ALTER/TRUNCATE). - Structures d'entrée dédiées +
DisallowUnknownFields(anti Mass-Assignment). - Erreurs métier stables (code + statut HTTP) et aucune fuite technique au client
(les
SQLSTATEsont traduits, jamais exposés bruts). - Suppression logique (horodatage
supprime_le) plutôt que destruction des données. - Transactions avec rollback automatique et verrouillage ciblé (
FOR UPDATE). - Arrêt gracieux (SIGINT/SIGTERM) qui laisse les requêtes en cours se terminer.
- Build Docker multi-stage, image minimale, exécution en utilisateur non-
root.
- Documentation
database/sql(Go) - Tutoriel officiel « Accessing a relational database »
- Go 1.22 : les nouveautés du routeur
net/http.ServeMux - Pilote
pgx(PostgreSQL pour Go) - Documentation officielle PostgreSQL
- Extension
pg_cron - The Twelve-Factor App
- OWASP API Security Top 10
- Spécification OpenAPI 3.0.3
Projet distribué sous licence MIT — libre d'utilisation, y compris à des fins pédagogiques et
commerciales. Voir le fichier LICENSE s'il est présent, ou adaptez la mention à votre contexte.