Retour au cours

backend / go

net/http et API REST

Leçon 201 exercice

Explication

Ce que vous allez apprendre

  • Construire une API REST complète avec la seule bibliothèque standard, sans framework
  • Router des requêtes par méthode et chemin avec http.ServeMux (Go 1.22+)
  • Décoder et encoder du JSON dans les handlers HTTP
  • Protéger un état partagé accédé concurremment par plusieurs requêtes
  • Écrire un middleware pour ajouter un comportement transverse (logging)

Dans quel contexte ?

Une petite équipe doit livrer rapidement une API de gestion de tâches, sans vouloir intégrer un framework tiers pour un projet aussi simple. Go permet exactement ça : construire une API REST fonctionnelle, robuste et performante avec uniquement net/http, sans dépendance externe — une possibilité rare parmi les langages backend modernes.

D'abord, un rappel important sur le contexte historique

Pendant longtemps, router des requêtes par méthode HTTP et paramètres de chemin (GET /taches/{id}) nécessitait un framework tiers comme Gin ou Chi, car http.ServeMux ne savait distinguer que des chemins simples. Depuis Go 1.22 (2024), ServeMux gère nativement les méthodes et les paramètres de chemin, rendant un framework externe optionnel pour bien des projets.

Une fois le routage posé, il faut structurer les réponses proprement

Chaque handler doit définir le Content-Type, le code de statut HTTP, puis sérialiser la réponse en JSON. Centraliser cette logique dans des fonctions utilitaires (repondreJSON, repondreErreur) évite de dupliquer ce boilerplate dans chaque handler.

Étape d'un handler RESTFonction typiquePiège si oubliée
Décoder le corps de la requêtejson.NewDecoder(r.Body).Decode(&payload)Payload invalide non détecté
Valider les donnéesVérification manuelle des champs requisDonnées incohérentes en base
Répondre avec le bon code HTTPhttp.StatusCreated, http.StatusNotFound...Client qui interprète mal la réponse
Protéger l'état partagésync.RWMutex sur le stockageRace condition sous forte charge

Prérequis

Cette leçon combine des notions déjà vues : structs et tags (via JSON, détaillé dans la prochaine leçon), sync.RWMutex (leçon sur sync), et gestion d'erreurs idiomatique.

Il reste un piège de conception fréquent chez les débutants sur les API Go

Oublier de protéger la structure Stockage (qui contient la map des tâches) avec un mutex fonctionne parfaitement en test avec une seule requête à la fois, puis provoque des race conditions dès que plusieurs clients frappent l'API simultanément — un défaut typique invisible tant que la charge reste faible.

Piège fréquent

Toujours vérifier, avant de considérer un handler HTTP "terminé", s'il accède à un état partagé entre requêtes (une map en mémoire, un compteur...). Si oui, ce champ doit être protégé par un sync.RWMutex, comme démontré dans la leçon précédente sur sync.

Enfin, un middleware n'est qu'une fonction qui enveloppe un handler

func middlewareLog(next http.Handler) http.Handler prend un handler existant et en retourne un nouveau qui ajoute un comportement (ici, journaliser chaque requête) avant de déléguer au handler d'origine. C'est un pattern simple mais extrêmement puissant, à la base de toute l'authentification, du logging et du rate limiting en Go.

Bonne pratique

Toujours valider explicitement le contenu d'un payload JSON reçu (champs requis, types cohérents) avant de l'utiliser : ne jamais faire confiance implicitement à ce qu'envoie un client, même interne à l'entreprise.

Maintenant que ton API répond en JSON, la prochaine leçon approfondit exactement ce mécanisme de sérialisation, avec tous les cas particuliers que encoding/json doit gérer en production.

Commandes & code

net/http et API REST

Construire une API REST avec la seule bibliothèque standard, sans framework.

go
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"strconv"
	"sync"
)

type Tache struct {
	ID     int    `json:"id"`
	Titre  string `json:"titre"`
	Faite  bool   `json:"faite"`
}

type Stockage struct {
	mu     sync.RWMutex
	taches map[int]Tache
	nextID int
}

func nouveauStockage() *Stockage {
	return &Stockage{taches: make(map[int]Tache), nextID: 1}
}

func (s *Stockage) Lister() []Tache {
	s.mu.RLock()
	defer s.mu.RUnlock()
	resultat := make([]Tache, 0, len(s.taches))
	for _, t := range s.taches {
		resultat = append(resultat, t)
	}
	return resultat
}

func (s *Stockage) Creer(titre string) Tache {
	s.mu.Lock()
	defer s.mu.Unlock()
	t := Tache{ID: s.nextID, Titre: titre}
	s.taches[t.ID] = t
	s.nextID++
	return t
}

func repondreJSON(w http.ResponseWriter, statut int, donnees any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statut)
	json.NewEncoder(w).Encode(donnees)
}

func repondreErreur(w http.ResponseWriter, statut int, message string) {
	repondreJSON(w, statut, map[string]string{"erreur": message})
}

// Handler via mux avec ServeMux (routage natif depuis Go 1.22, avec methodes/params)
func creerRoutes(s *Stockage) *http.ServeMux {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /taches", func(w http.ResponseWriter, r *http.Request) {
		repondreJSON(w, http.StatusOK, s.Lister())
	})

	mux.HandleFunc("POST /taches", func(w http.ResponseWriter, r *http.Request) {
		var payload struct {
			Titre string `json:"titre"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			repondreErreur(w, http.StatusBadRequest, "corps JSON invalide")
			return
		}
		if payload.Titre == "" {
			repondreErreur(w, http.StatusUnprocessableEntity, "titre requis")
			return
		}
		t := s.Creer(payload.Titre)
		repondreJSON(w, http.StatusCreated, t)
	})

	mux.HandleFunc("GET /taches/{id}", func(w http.ResponseWriter, r *http.Request) {
		id, err := strconv.Atoi(r.PathValue("id"))
		if err != nil {
			repondreErreur(w, http.StatusBadRequest, "id invalide")
			return
		}
		s.mu.RLock()
		t, ok := s.taches[id]
		s.mu.RUnlock()
		if !ok {
			repondreErreur(w, http.StatusNotFound, "tache non trouvee")
			return
		}
		repondreJSON(w, http.StatusOK, t)
	})

	return mux
}

// Middleware : fonction qui enveloppe un handler pour ajouter un comportement
func middlewareLog(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s %s", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
	})
}

func main() {
	stockage := nouveauStockage()
	mux := creerRoutes(stockage)

	handler := middlewareLog(mux)

	log.Println("serveur demarre sur :8080")
	log.Fatal(http.ListenAndServe(":8080", handler))
}

Résumé

  • http.ServeMux (Go 1.22+) route par méthode et chemin, avec des paramètres {id} natifs.
  • Un middleware est simplement une fonction func(http.Handler) http.Handler.
  • Toujours vérifier Content-Type, valider le payload, retourner les bons codes HTTP.
  • Protéger l'état partagé (Stockage) avec sync.RWMutex dès qu'il est accédé concurremment.

Exercices pratiques

1 disponible
1

Mission : une API de tâches qui plante sous charge en CI mais jamais en local

Objectif : Sécuriser un nouvel endpoint de suppression contre les races conditions, et éviter un piège de contention de verrou en production.

Contexte

Un développeur ajoute rapidement DELETE /taches/{id} à l'API de gestion de tâches en s'inspirant du style des autres handlers, mais sans relire attentivement comment Stockage protège son état. Le pipeline CI, qui lance les tests avec go test -race, échoue systématiquement sur ce nouvel endpoint, alors qu'aucun bug n'apparaît jamais en développement local avec un seul client à la fois.

Résoudre l’exercice →