Leçon 10/11 8 min

HTTP et API REST

Créez un serveur web et une API REST avec la bibliothèque standard de Go : net/http, JSON, handlers.

Un serveur web en 6 lignes

La bibliothèque standard de Go inclut tout ce qu'il faut pour créer un serveur web. Pas besoin de framework externe :

Prédisez avant de lire

Un handler HTTP en Go a la signature func(w http.ResponseWriter, r *http.Request) et ne renvoie aucune valeur. Avant de dérouler : comment envoie-t-on alors la réponse au client, et à quoi servent précisément w et r ?

Voir la réponse

Un handler Go ne renvoie rien (pas de valeur de retour). On envoie la réponse en écrivant dans w, le http.ResponseWriter : par exemple fmt.Fprintf(w, "Bonjour") ou w.Write([]byte("...")). w est le flux de sortie vers le client : on y écrit le corps, on règle le code de statut avec w.WriteHeader(...) et les en-têtes avec w.Header(). r, le *http.Request, contient tout ce qui vient du client : la méthode, l'URL, les paramètres (r.URL.Query()), les en-têtes, le corps. Modèle mental : on lit la requête dans r, on écrit la réponse dans w. Si on n'écrit rien dans w, le client reçoit une réponse 200 OK vide.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Bonjour depuis Go !")
    })
    http.ListenAndServe(":8080", nil)
}

Lancez avec go run main.go et ouvrez http://localhost:8080. Votre serveur est en ligne.

En Python, il faudrait Flask ou Django. En JavaScript, Express. En Go, la bibliothèque standard suffit. Et ce serveur gère chaque requête dans sa propre goroutine : concurrence gratuite.

Le client envoie une requête au serveur net/http, qui la route vers le bon handler dans sa propre goroutine ; le handler écrit la réponse dans w, renvoyée au client. Client navigateur net/http route → handler (goroutine) handler(w, r) écrit dans w requête réponse
Une requête entre, net/http la route vers le bon handler (dans sa propre goroutine), qui écrit la réponse dans w.

En production, on configure le serveur. http.HandleFunc + ListenAndServe(":8080", nil) reposent sur un routeur global, pratique pour apprendre. En réel, on crée un http.ServeMux explicite et un http.Server avec des timeouts (sinon une connexion lente peut monopoliser une ressource indéfiniment) :

mux := http.NewServeMux()
mux.HandleFunc("/api/tasks", handleTasks)

srv := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
srv.ListenAndServe()

Plusieurs routes

Ajoutez autant de routes que nécessaire avec http.HandleFunc :

func main() {
    http.HandleFunc("/", handleHome)
    http.HandleFunc("/api/health", handleHealth)
    http.HandleFunc("/api/users", handleUsers)

    fmt.Println("Serveur démarré sur :8080")
    http.ListenAndServe(":8080", nil)
}

func handleHome(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Page d'accueil")
}

func handleHealth(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"status": "ok"}`)
}

func handleUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `[{"name": "Alice"}, {"name": "Bob"}]`)
}

Chaque handler reçoit deux paramètres :

  • w http.ResponseWriter : pour écrire la réponse
  • r *http.Request : la requête entrante (méthode, URL, body, headers)

JSON : encoding/json

Le package encoding/json convertit les structs Go en JSON et inversement :

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// Struct → JSON (Marshal)
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

// JSON → Struct (Unmarshal)
func handleCreateUser(w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, "JSON invalide", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "Utilisateur créé : %s", user.Name)
}

Les tags `json:"name"` après chaque champ contrôlent le nom dans le JSON. Sans eux, Go utiliserait Name avec une majuscule.

Une API REST complète

Combinons tout pour créer une API de gestion de tâches :

type Task struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

var tasks = []Task{
    {ID: 1, Title: "Apprendre Go", Done: false},
    {ID: 2, Title: "Créer une API", Done: false},
}

func handleTasks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {
    case "GET":
        json.NewEncoder(w).Encode(tasks)
    case "POST":
        var task Task
        if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
            http.Error(w, "JSON invalide", http.StatusBadRequest)
            return
        }
        task.ID = len(tasks) + 1
        tasks = append(tasks, task)
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(task)
    default:
        http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
    }
}

Pour rester simple, cet exemple ne protège pas la variable tasks contre les accès concurrents. Or net/http traite chaque requête dans sa propre goroutine : deux POST simultanés provoqueraient une data race sur le slice. En production, on protégerait tasks avec un sync.Mutex (voir la leçon Goroutines et channels).

Cette API gère GET /api/tasks pour lister et POST /api/tasks pour créer. Testez-la avec curl :

# Lister les tâches
curl http://localhost:8080/api/tasks

# Créer une tâche
curl -X POST -d '{"title":"Première tâche"}' http://localhost:8080/api/tasks

À vous d'essayer (le serveur HTTP ne tourne pas ici, mais l'encodage JSON oui) :

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

func main() {
    user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    data, _ := json.Marshal(user)
    fmt.Println(string(data))
}

A web server in 6 lines

Go's standard library includes everything you need to create a web server. No external framework required:

Predict before reading

An HTTP handler in Go has the signature func(w http.ResponseWriter, r *http.Request) and returns no value. Before scrolling: how do you send the response to the client then, and what exactly are w and r for?

See the answer

A Go handler returns nothing (no return value). You send the response by writing into w, the http.ResponseWriter: for example fmt.Fprintf(w, "Hello") or w.Write([]byte("...")). w is the output stream to the client: you write the body there, set the status code with w.WriteHeader(...), and headers with w.Header(). r, the *http.Request, holds everything coming from the client: the method, URL, query parameters (r.URL.Query()), headers, and body. Mental model: you read the request from r, you write the response into w. If you write nothing into w, the client receives an empty 200 OK response.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go!")
    })
    http.ListenAndServe(":8080", nil)
}

Run with go run main.go and open http://localhost:8080. Your server is live.

In Python, you'd need Flask or Django. In JavaScript, Express. In Go, the standard library is enough. And this server handles each request in its own goroutine — free concurrency.

The client sends a request to the net/http server, which routes it to the right handler in its own goroutine; the handler writes the response into w, returned to the client. Client browser net/http route → handler (goroutine) handler(w, r) writes into w request response
A request comes in, net/http routes it to the right handler (in its own goroutine), which writes the response into w.

In production, you configure the server. http.HandleFunc + ListenAndServe(":8080", nil) rely on a global router, handy for learning. In the real world you create an explicit http.ServeMux and an http.Server with timeouts (otherwise a slow connection can tie up a resource indefinitely):

mux := http.NewServeMux()
mux.HandleFunc("/api/tasks", handleTasks)

srv := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
srv.ListenAndServe()

Multiple routes

Add as many routes as needed with http.HandleFunc:

func main() {
    http.HandleFunc("/", handleHome)
    http.HandleFunc("/api/health", handleHealth)
    http.HandleFunc("/api/users", handleUsers)

    fmt.Println("Server started on :8080")
    http.ListenAndServe(":8080", nil)
}

func handleHome(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Home page")
}

func handleHealth(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"status": "ok"}`)
}

func handleUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `[{"name": "Alice"}, {"name": "Bob"}]`)
}

Each handler receives two parameters:

  • w http.ResponseWriter — to write the response
  • r *http.Request — the incoming request (method, URL, body, headers)

JSON: encoding/json

The encoding/json package converts Go structs to JSON and vice versa:

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// Struct → JSON (Marshal)
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

// JSON → Struct (Unmarshal)
func handleCreateUser(w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "User created: %s", user.Name)
}

The tags `json:"name"` after each field control the name in JSON. Without them, Go would use Name with a capital letter.

A complete REST API

Let's combine everything to create a task management API:

type Task struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

var tasks = []Task{
    {ID: 1, Title: "Learn Go", Done: false},
    {ID: 2, Title: "Create an API", Done: false},
}

func handleTasks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {
    case "GET":
        json.NewEncoder(w).Encode(tasks)
    case "POST":
        var task Task
        if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
            http.Error(w, "Invalid JSON", http.StatusBadRequest)
            return
        }
        task.ID = len(tasks) + 1
        tasks = append(tasks, task)
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(task)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

To keep it simple, this example doesn't protect the tasks variable against concurrent access. But net/http serves each request in its own goroutine: two simultaneous POSTs would cause a data race on the slice. In production you'd guard tasks with a sync.Mutex (see the Goroutines and channels lesson).

This API handles GET /api/tasks to list and POST /api/tasks to create. Test it with curl:

# List tasks
curl http://localhost:8080/api/tasks

# Create a task
curl -X POST -d '{"title":"Test the API"}' http://localhost:8080/api/tasks

Try it yourself (the HTTP server doesn't run here, but JSON encoding does):

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

func main() {
    user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    data, _ := json.Marshal(user)
    fmt.Println(string(data))
}

Le serveur HTTP ne fonctionne pas dans le Playground ni dans un sandbox en ligne. Installez Go localement pour tester : https://go.dev/dl/

🎯 Pratique

S'entraîner (clique pour ouvrir) :

Prompt IA
Avec l'IA

Copiez ce prompt dans Claude ou ChatGPT :

Crée une API REST Go complète pour gérer des livres (CRUD). Utilise uniquement la bibliothèque standard. Ajoute la validation des données et des codes de statut HTTP appropriés.
💬 Ré-explique sans regarder
Ré-explique sans regarder

Sans relire le code de l'IA : décris de mémoire ce que fait un handler POST qui crée une ressource, de la lecture du r.Body jusqu'à la réponse. Quels packages et quel code de statut utilises-tu ?

Une bonne explication dit : on décode le corps avec json.NewDecoder(r.Body).Decode(&task) (package encoding/json) ; on vérifie l'erreur retournée et on répond http.StatusBadRequest (400) si le JSON est invalide ; en cas de succès on écrit Content-Type: application/json, on renvoie http.StatusCreated (201) avec w.WriteHeader, puis on encode la ressource créée avec json.NewEncoder(w).Encode(...). Le routage et la méthode viennent de net/http.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

L'IA te propose ce handler POST. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.

func handleCreateTask(w http.ResponseWriter, r *http.Request) {
    var task Task
    json.NewDecoder(r.Body).Decode(&task)
    task.ID = len(tasks) + 1
    tasks = append(tasks, task)
    json.NewEncoder(w).Encode(task)
}
À rejeter. L'erreur de Decode est ignorée : si le client envoie un JSON invalide, task reste vide et on enregistre quand même une tâche vide au lieu de répondre 400 Bad Request. Réflexe Go : on ne jette jamais une error retournée. Il faut if err := json.NewDecoder(r.Body).Decode(&task); err != nil { http.Error(w, "JSON invalide", http.StatusBadRequest); return }. Bonus : pense aussi à w.WriteHeader(http.StatusCreated) avant d'encoder, sinon la réponse part en 200 alors qu'on a créé une ressource.
🧠 Rappel libre
Rappel libre

Sans remonter dans la leçon : quelle est la signature d'un handler HTTP en Go, et avec quelle fonction renvoies-tu un struct au format JSON dans la réponse ?

La signature est func(w http.ResponseWriter, r *http.Request) : w sert à écrire la réponse, r contient la requête entrante. Pour renvoyer un struct en JSON, on pose d'abord l'en-tête w.Header().Set("Content-Type", "application/json") puis on appelle json.NewEncoder(w).Encode(monStruct) (ou json.Marshal qui retourne un []byte).
Quel package Go permet de créer un serveur HTTP ?
Comment convertir un struct Go en JSON ?
À quoi servent les tags json:"..." sur les champs d'un struct ?
Comment Go gère-t-il les requêtes HTTP concurrentes ?
Prochaine étape

Votre API REST répond en JSON, mais comment savoir qu'elle ne casse pas au prochain changement ? Dans la dernière leçon, vous écrivez des tests avec le package testing intégré et vous mesurez les performances avec les benchmarks.

Leçon 11 : Tests et benchmarks →

Une erreur dans cette leçon, un passage flou, une question ? Écrivez-moi : chaque retour améliore ce cours.

Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement