Développement Web

Construire une première API REST en Go avec net/http

13 دقائق للقراءة
📍 Article principal du parcours : Go (Golang) : le guide complet pour débuter
Ce tutoriel fait partie du parcours « Go de zéro ». Pour la vue d’ensemble, lisez d’abord le guide principal.

Construire une première API REST en Go avec net/http

GareBook sait modéliser des trajets, les gérer en mémoire et les vérifier en parallèle. Mais tout cela reste enfermé dans un terminal. Pour qu’une application mobile, un site web ou un autre service puisse réserver une place, il faut exposer GareBook sur le réseau, en HTTP. C’est le rôle d’une API REST : des adresses (GET /trajets, POST /reservations) qui échangent des données en JSON.

La bonne nouvelle : Go fait ça avec sa seule bibliothèque standard, sans installer le moindre framework. Et depuis Go 1.22, son routeur intégré gère proprement les méthodes HTTP et les paramètres d’URL, ce qui rendait autrefois nécessaire une bibliothèque tierce. À la fin de ce tutoriel, vous aurez une API qui répond à de vraies requêtes, testable depuis votre navigateur ou avec curl.

🎯 Ce que vous allez apprendre

  • Démarrer un serveur HTTP avec net/http et le routeur ServeMux ;
  • Définir des routes par méthode et par chemin (GET /trajets/{id}) ;
  • Encoder et décoder du JSON avec le package encoding/json ;
  • Renvoyer les bons codes de statut HTTP et gérer les erreurs côté serveur.

🛠️ Ce que vous allez construire

Une API HTTP pour GareBook avec trois routes : lister tous les trajets, consulter un trajet par son identifiant, et réserver une place. Le tout en JSON, avec gestion des cas d’erreur (trajet introuvable, trajet complet). Vous l’interrogerez en direct depuis votre terminal.

Prérequis

  • Avoir suivi les tutoriels précédents, en particulier structs, slices et erreurs ;
  • Disposer du type Trajet et d’une liste de trajets en mémoire ;
  • Avoir curl (présent par défaut sur Windows 10+, macOS et Linux) pour tester ;
  • Niveau : débutant à l’aise. Test express : si vous savez ce qu’est une requête GET, vous êtes prêt.
  • ⏱️ Temps estimé : ~45 minutes.

Étape 1 — Un serveur HTTP minimal

Commençons par le plus petit serveur possible, pour valider que la mécanique répond. On crée un routeur (http.NewServeMux), on lui associe une fonction pour une route, et on lance le serveur sur un port. Une fonction qui traite une requête reçoit toujours deux paramètres : un ResponseWriter pour écrire la réponse, et un *Request qui décrit la requête entrante.

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /sante", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "GareBook est en ligne")
	})

	log.Println("Serveur démarré sur http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Quelques points. "GET /sante" : depuis Go 1.22, on précise la méthode HTTP directement dans le motif de route — pratique et lisible. http.ListenAndServe(":8080", mux) lance le serveur sur le port 8080 ; il bloque tant que le serveur tourne. log.Fatal affiche et arrête le programme si le serveur ne peut pas démarrer (port déjà pris, par exemple). Lancez go run ., puis dans un autre terminal :

curl http://localhost:8080/sante
# → GareBook est en ligne

Point d’étapecurl (ou votre navigateur sur http://localhost:8080/sante) renvoie le message de santé. Le serveur tourne dans le terminal ; faites Ctrl+C pour l’arrêter. Si le port est occupé, changez :8080 en :8081.

Étape 2 — Renvoyer du JSON

Une API REST échange du JSON, pas du texte brut. Le package encoding/json transforme une struct Go en JSON et inversement. Première chose : on enrichit le type Trajet avec des tags qui dictent les noms des champs en JSON. Sans eux, Go utiliserait les noms Go (avec majuscules) ; les tags donnent des noms propres et conventionnels :

type Trajet struct {
	ID       int    `json:"id"`
	Depart   string `json:"depart"`
	Arrivee  string `json:"arrivee"`
	PrixFCFA int    `json:"prix_fcfa"`
	Places   int    `json:"places"`
}

Rappel important : seuls les champs commençant par une majuscule sont exportés, donc visibles par encoding/json. Un champ en minuscule serait ignoré à la sérialisation. Écrivons maintenant la route qui liste tous les trajets en JSON. On écrit l’en-tête Content-Type, puis on encode le slice directement dans la réponse :

var trajets = []Trajet{
	{ID: 1, Depart: "Dakar", Arrivee: "Saint-Louis", PrixFCFA: 3600, Places: 7},
	{ID: 2, Depart: "Dakar", Arrivee: "Thiès", PrixFCFA: 1500, Places: 0},
	{ID: 3, Depart: "Thiès", Arrivee: "Touba", PrixFCFA: 2000, Places: 12},
}

func listerTrajets(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(trajets); err != nil {
		http.Error(w, "erreur d'encodage", http.StatusInternalServerError)
	}
}

json.NewEncoder(w).Encode(trajets) écrit le JSON directement dans la réponse, sans passer par une variable intermédiaire — efficace en mémoire. N’oubliez pas import "encoding/json". Branchez la route dans main avec mux.HandleFunc("GET /trajets", listerTrajets), relancez, et testez :

curl http://localhost:8080/trajets
# → [{"id":1,"depart":"Dakar","arrivee":"Saint-Louis","prix_fcfa":3600,"places":7}, ...]

Étape 3 — Lire un paramètre d’URL

Pour consulter un trajet précis, l’URL contient son identifiant : GET /trajets/3. Go 1.22 permet de capturer ce segment avec une accolade dans le motif, puis de le lire avec r.PathValue. C’est exactement ce qui demandait un routeur tiers auparavant, désormais intégré :

import "strconv"

func voirTrajet(w http.ResponseWriter, r *http.Request) {
	idStr := r.PathValue("id")        // le segment {id} de l'URL
	id, err := strconv.Atoi(idStr)    // texte → entier
	if err != nil {
		http.Error(w, "identifiant invalide", http.StatusBadRequest)
		return
	}

	for _, t := range trajets {
		if t.ID == id {
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(t)
			return
		}
	}
	http.Error(w, "trajet introuvable", http.StatusNotFound)
}

On lit le paramètre {id} avec r.PathValue("id"), on le convertit en entier avec strconv.Atoi (qui renvoie une erreur si ce n’est pas un nombre), et on cherche le trajet. Trois réponses possibles : 400 si l’ID n’est pas un nombre, 404 si le trajet n’existe pas, ou le trajet en JSON sinon. Ces codes de statut sont le langage des API REST : ils disent à l’appelant ce qui s’est passé. Branchez mux.HandleFunc("GET /trajets/{id}", voirTrajet) et testez :

curl http://localhost:8080/trajets/3      # → le trajet Thiès → Touba
curl -i http://localhost:8080/trajets/99  # → HTTP/1.1 404 Not Found

Point d’étapeGET /trajets renvoie la liste, GET /trajets/3 un trajet précis, et un ID inexistant renvoie un code 404 (l’option -i de curl affiche l’en-tête de statut). Vous tenez les deux requêtes de lecture d’une API REST.

Étape 4 — Recevoir des données : réserver une place

Jusqu’ici on ne faisait que lire. Réserver une place, c’est écrire : le client envoie une requête POST avec un corps JSON, le serveur le décode, agit, et répond. On décode le corps de la requête avec json.NewDecoder(r.Body). Voici la route de réservation :

type DemandeReservation struct {
	TrajetID int `json:"trajet_id"`
}

func reserver(w http.ResponseWriter, r *http.Request) {
	var d DemandeReservation
	if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
		http.Error(w, "JSON invalide", http.StatusBadRequest)
		return
	}

	for i := range trajets {
		if trajets[i].ID == d.TrajetID {
			if trajets[i].Places <= 0 {
				http.Error(w, "trajet complet", http.StatusConflict)
				return
			}
			trajets[i].Places-- // une place de moins
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusCreated) // 201 : ressource créée
			json.NewEncoder(w).Encode(trajets[i])
			return
		}
	}
	http.Error(w, "trajet introuvable", http.StatusNotFound)
}

Détail crucial : on parcourt avec for i := range trajets et on modifie trajets[i] par son indice, car modifier la copie t d’un for _, t := range ne changerait rien dans le slice d’origine. On renvoie 201 Created en cas de succès, 409 Conflict si le trajet est complet, 404 s’il n’existe pas. Branchez mux.HandleFunc("POST /reservations", reserver) et testez l’envoi d’un corps JSON :

curl -X POST http://localhost:8080/reservations \
  -H "Content-Type: application/json" \
  -d '{"trajet_id": 1}'
# → le trajet 1 avec une place de moins, statut 201

Réservez plusieurs fois le trajet 2 (qui a 0 place) : vous obtiendrez un 409 Conflict « trajet complet ». L’API se comporte comme un vrai service.

Étape 5 — Vérification finale

Votre main doit maintenant enregistrer les quatre routes (/sante, GET /trajets, GET /trajets/{id}, POST /reservations) sur le même ServeMux, puis lancer ListenAndServe. Relancez go run . et déroulez le scénario complet avec curl : lister, consulter, réserver, constater la baisse des places, tomber sur un trajet complet. Si chaque requête renvoie le bon JSON et le bon code de statut, votre première API REST en Go est fonctionnelle. Vous venez de faire, avec la seule bibliothèque standard, ce que beaucoup croient impossible sans framework.

Un dernier réflexe à connaître, même si on ne le code pas en détail ici : le middleware. C’est une fonction qui enveloppe vos handlers pour ajouter un comportement transversal — journaliser chaque requête, mesurer son temps de réponse, vérifier une clé d’authentification — sans dupliquer ce code dans chaque route. En Go, un middleware prend un http.Handler et en renvoie un autre, enrichi. Concrètement, on écrirait une fonction qui, avant d’appeler le handler, note dans les logs la méthode et le chemin de la requête, puis mesure la durée après. C’est ainsi qu’on garde une trace de qui appelle GareBook et à quelle vitesse l’API répond — indispensable dès qu’un service tourne en production. La version avancée liée en fin de page montre comment chaîner plusieurs middlewares proprement.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
Le JSON renvoyé a des champs en majuscules ou vides Champs non exportés (minuscule) ou tags json manquants Mettez les champs en majuscule et ajoutez les tags `json:"nom"`
La réservation ne décrémente pas les places Vous modifiez la copie t d’un for _, t := range Parcourez par indice : for i := range trajets puis trajets[i]
404 page not found sur une route pourtant définie Méthode HTTP non précisée, ou faute dans le motif Vérifiez "GET /trajets" (méthode + espace + chemin)
EOF au décodage JSON Corps de requête vide ou mal formé Envoyez un corps JSON valide ; testez avec l’en-tête Content-Type
address already in use au démarrage Le port 8080 est déjà occupé (ancien serveur encore actif) Arrêtez l’ancien (Ctrl+C) ou changez de port

🌍 Adaptation au contexte ouest-africain

Une API Go compilée tient sans peine sur un VPS d’entrée de gamme et répond vite, même avec peu de RAM — exactement le profil d’hébergement abordable visé par un freelance ou une jeune structure. Comme le binaire est autonome, vous le déposez sur le serveur et le lancez derrière un reverse proxy (Nginx) qui gère le HTTPS ; le tutoriel de déploiement détaille cette mise en ligne.

Un point d’attention pour le contexte local : notre API stocke les trajets en mémoire, donc tout disparaît à chaque redémarrage. Pour un service réel, exposé à des coupures de courant, vous brancherez une base de données. La structure en place (des routes claires, un type Trajet bien défini) facilitera ce branchement. Pensez aussi à valider rigoureusement les données reçues : ne faites jamais confiance à un corps de requête venu de l’extérieur.

✅ Récapitulatif

Vous avez construit une API REST complète avec la seule bibliothèque standard de Go. Vous savez démarrer un serveur avec net/http et son ServeMux, définir des routes par méthode et par chemin (GET /trajets/{id}), lire un paramètre d’URL avec r.PathValue, encoder et décoder du JSON avec encoding/json, et renvoyer les bons codes de statut HTTP. GareBook est désormais un vrai service réseau. Il ne reste qu’à le tester, le compiler et le mettre en ligne.

🧾 Aide-mémoire

Élément Rôle
http.NewServeMux() Crée le routeur
mux.HandleFunc("GET /x/{id}", h) Route par méthode + chemin avec paramètre
r.PathValue("id") Lit un paramètre d’URL (Go 1.22+)
json.NewEncoder(w).Encode(v) Écrit v en JSON dans la réponse
json.NewDecoder(r.Body).Decode(&v) Lit le corps JSON de la requête
w.WriteHeader(http.StatusCreated) Fixe le code de statut (201)
http.Error(w, msg, code) Réponse d’erreur avec message et code
http.ListenAndServe(":8080", mux) Démarre le serveur

💪 À vous de jouer

Ajoutez une route GET /trajets/{id}/places qui renvoie uniquement le nombre de places restantes d’un trajet, au format JSON {"places": 7}. Gérez le cas du trajet introuvable avec un 404.

Voir une solution
func placesTrajet(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.PathValue("id"))
	if err != nil {
		http.Error(w, "identifiant invalide", http.StatusBadRequest)
		return
	}
	for _, t := range trajets {
		if t.ID == id {
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]int{"places": t.Places})
			return
		}
	}
	http.Error(w, "trajet introuvable", http.StatusNotFound)
}

// Dans main :
// mux.HandleFunc("GET /trajets/{id}/places", placesTrajet)

On réutilise PathValue et Atoi, et on encode une petite map map[string]int directement en JSON pour une réponse compacte.

Tutoriels frères

Pour aller plus loin

FAQ

Ai-je vraiment besoin d’un framework (Gin, Fiber, Echo) ?
Pas pour commencer, et souvent pas du tout. Depuis Go 1.22, net/http couvre les méthodes, les paramètres d’URL et le JSON. Les frameworks ajoutent du confort (middlewares prêts à l’emploi, validation) mais aussi une dépendance. Maîtrisez d’abord la base : vous comprendrez ensuite ce qu’un framework vous apporte réellement.

Comment gérer plusieurs requêtes en même temps ?
C’est automatique : net/http traite chaque requête dans sa propre goroutine. Mais attention — si plusieurs requêtes modifient les mêmes données (comme notre slice trajets), vous avez une course de données. En production, protégez l’accès avec un Mutex (voir le tutoriel sur la concurrence) ou une base de données.

Pourquoi mes champs JSON n’apparaissent pas ?
Parce qu’ils commencent par une minuscule. encoding/json ne voit que les champs exportés (majuscule). Mettez la majuscule et contrôlez le nom JSON avec un tag `json:"mon_champ"`.

Quel code de statut renvoyer ?
200 pour une lecture réussie, 201 pour une création, 400 pour une requête mal formée, 404 pour une ressource absente, 409 pour un conflit (trajet complet), 500 pour une erreur serveur. Ces codes sont le contrat d’une API REST.

Mots-clés : API REST Go, net/http, ServeMux, encoding/json, PathValue, codes statut HTTP, Go 1.22 routing, débuter Golang.

مشاركة