Tous les articles
go20 juin 2026 20 min

API REST : guide complet avec exemples Go, Python et Next.js

Guide complet API REST : principes fondamentaux, conception d'URLs, codes HTTP, implémentation Go (Gin) et Python (FastAPI) avec authentification JWT, pagination et gestion d'erreurs. Consommation depuis Next.js.

L'API REST est le standard de communication entre services web. Que vous construisiez une application Next.js, une app mobile, ou un système de microservices, vous aurez besoin de concevoir et consommer des APIs REST. Ce guide complet couvre tout : les principes fondamentaux, la conception, et l'implémentation concrète en Go, Python et Node.js.

Qu'est-ce qu'une API REST ?

REST (Representational State Transfer) est un style d'architecture pour les services web, défini par Roy Fielding dans sa thèse de 2000. Une API REST expose des ressources via des URLs et utilise les méthodes HTTP standard pour les manipuler.
Ce n'est pas un protocole ni un format — c'est un ensemble de contraintes architecturales. Une API qui respecte ces contraintes est dite "RESTful".

Les 6 contraintes REST

  • Client-Serveur : séparation stricte entre interface et logique métier
  • Sans état (Stateless) : chaque requête contient toutes les infos nécessaires, le serveur ne mémorise rien entre les appels
  • Mise en cache (Cacheable) : les réponses indiquent si elles peuvent être cachées
  • Interface uniforme : URLs cohérentes, méthodes HTTP standard, format prévisible
  • Système en couches : le client ne sait pas s'il parle directement au serveur ou à un proxy
  • Code à la demande (optionnel) : le serveur peut envoyer du code exécutable

Les méthodes HTTP — Sémantique exacte

Chaque méthode HTTP a une sémantique précise que vous devez respecter pour que votre API soit vraiment RESTful :
  • GET — Lire une ressource. Idempotent et sans effet de bord. Peut être mis en cache.
  • POST — Créer une nouvelle ressource ou déclencher une action. Non idempotent.
  • PUT — Remplacer entièrement une ressource existante. Idempotent.
  • PATCH — Modifier partiellement une ressource. Comportement partiel.
  • DELETE — Supprimer une ressource. Idempotent.
  • HEAD — Comme GET mais sans le corps de réponse. Utile pour vérifier l'existence.
  • OPTIONS — Récupérer les méthodes supportées. Utilisé pour le CORS preflight.
Idempotent = appeler la même opération 1 fois ou 10 fois produit le même résultat. GET /users/1 toujours retourne le même user. DELETE /users/1 supprime l'user, les appels suivants retournent 404 — le résultat final est le même : l'user n'existe plus.

Conception d'une API REST — Les bonnes pratiques

Nommage des URLs

# ✅ Bonnes URLs REST
GET    /users              # Lister tous les utilisateurs
GET    /users/42           # Récupérer l'utilisateur 42
POST   /users              # Créer un utilisateur
PUT    /users/42           # Remplacer l'utilisateur 42
PATCH  /users/42           # Modifier partiellement l'utilisateur 42
DELETE /users/42           # Supprimer l'utilisateur 42

# Ressources imbriquées
GET    /users/42/orders    # Commandes de l'utilisateur 42
GET    /users/42/orders/7  # Commande 7 de l'utilisateur 42
POST   /users/42/orders    # Créer une commande pour l'utilisateur 42

# ❌ Mauvaises URLs — verbes dans l'URL
GET    /getUser/42         # Non, utilisez GET /users/42
POST   /createUser         # Non, utilisez POST /users
GET    /deleteUser/42      # Non, utilisez DELETE /users/42
POST   /users/42/activate  # Cas particulier acceptable pour les actions

Les codes HTTP — Sémantique précise

  • 200 OK — Succès général (GET, PUT, PATCH)
  • 201 Created — Ressource créée (POST). Inclure Location: /users/42 dans les headers.
  • 204 No Content — Succès sans corps de réponse (DELETE)
  • 400 Bad Request — Requête malformée, données invalides
  • 401 Unauthorized — Non authentifié (token manquant ou invalide)
  • 403 Forbidden — Authentifié mais pas autorisé
  • 404 Not Found — Ressource inexistante
  • 409 Conflict — Conflit (email déjà utilisé, version obsolète)
  • 422 Unprocessable Entity — Données bien formées mais invalides sémantiquement
  • 429 Too Many Requests — Rate limiting
  • 500 Internal Server Error — Erreur serveur inattendue

Implémenter une API REST en Go avec Gin

Go est l'un des meilleurs langages pour les APIs REST : performances natives, démarrage rapide, typage fort. Gin est le framework HTTP le plus populaire de l'écosystème Go.

Structure du projet

api/
├── main.go
├── handler/
│   ├── user.go
│   └── order.go
├── middleware/
│   ├── auth.go
│   ├── cors.go
│   └── ratelimit.go
├── model/
│   ├── user.go
│   └── response.go
├── repository/
│   └── user_postgres.go
└── router/
    └── router.go

Modèles et réponses standardisées

// model/response.go
package model

import "time"

// APIResponse — enveloppe standardisée pour toutes les réponses
type APIResponse struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Error   *APIError   `json:"error,omitempty"`
    Meta    *Meta       `json:"meta,omitempty"`
}

type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Details []string `json:"details,omitempty"`
}

type Meta struct {
    Page       int `json:"page"`
    PerPage    int `json:"per_page"`
    Total      int `json:"total"`
    TotalPages int `json:"total_pages"`
}

// model/user.go
type User struct {
    ID        string    `json:"id"`
    Email     string    `json:"email" binding:"required,email"`
    Name      string    `json:"name" binding:"required,min=2"`
    Role      string    `json:"role"`
    CreatedAt time.Time `json:"created_at"`
}

type CreateUserRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Name     string `json:"name" binding:"required,min=2,max=100"`
    Password string `json:"password" binding:"required,min=8"`
}

type UpdateUserRequest struct {
    Name  string `json:"name,omitempty" binding:"omitempty,min=2"`
    Email string `json:"email,omitempty" binding:"omitempty,email"`
}

Handlers CRUD complets

// handler/user.go
package handler

import (
    "net/http"
    "strconv"
    "myapi/model"
    "myapi/repository"
    "github.com/gin-gonic/gin"
)

type UserHandler struct {
    repo repository.UserRepository
}

func NewUserHandler(repo repository.UserRepository) *UserHandler {
    return &UserHandler{repo: repo}
}

// GET /users
func (h *UserHandler) List(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
    search := c.Query("search")

    if page < 1 { page = 1 }
    if perPage > 100 { perPage = 100 }

    users, total, err := h.repo.FindAll(page, perPage, search)
    if err != nil {
        c.JSON(http.StatusInternalServerError, model.APIResponse{
            Error: &model.APIError{Code: "DB_ERROR", Message: "Erreur base de données"},
        })
        return
    }

    totalPages := (total + perPage - 1) / perPage
    c.JSON(http.StatusOK, model.APIResponse{
        Success: true,
        Data:    users,
        Meta: &model.Meta{
            Page: page, PerPage: perPage,
            Total: total, TotalPages: totalPages,
        },
    })
}

// GET /users/:id
func (h *UserHandler) Get(c *gin.Context) {
    user, err := h.repo.FindByID(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusInternalServerError, model.APIResponse{
            Error: &model.APIError{Code: "DB_ERROR", Message: err.Error()},
        })
        return
    }
    if user == nil {
        c.JSON(http.StatusNotFound, model.APIResponse{
            Error: &model.APIError{Code: "NOT_FOUND", Message: "Utilisateur introuvable"},
        })
        return
    }
    c.JSON(http.StatusOK, model.APIResponse{Success: true, Data: user})
}

// POST /users
func (h *UserHandler) Create(c *gin.Context) {
    var req model.CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, model.APIResponse{
            Error: &model.APIError{
                Code:    "VALIDATION_ERROR",
                Message: "Données invalides",
                Details: parseValidationErrors(err),
            },
        })
        return
    }

    // Vérifier unicité email
    existing, _ := h.repo.FindByEmail(req.Email)
    if existing != nil {
        c.JSON(http.StatusConflict, model.APIResponse{
            Error: &model.APIError{
                Code:    "EMAIL_TAKEN",
                Message: "Cet email est déjà utilisé",
            },
        })
        return
    }

    user, err := h.repo.Create(req)
    if err != nil {
        c.JSON(http.StatusInternalServerError, model.APIResponse{
            Error: &model.APIError{Code: "CREATE_FAILED", Message: err.Error()},
        })
        return
    }

    // 201 Created avec Location header
    c.Header("Location", "/users/"+user.ID)
    c.JSON(http.StatusCreated, model.APIResponse{Success: true, Data: user})
}

// PATCH /users/:id
func (h *UserHandler) Update(c *gin.Context) {
    var req model.UpdateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, model.APIResponse{
            Error: &model.APIError{Code: "VALIDATION_ERROR", Message: err.Error()},
        })
        return
    }

    user, err := h.repo.Update(c.Param("id"), req)
    if err != nil {
        c.JSON(http.StatusInternalServerError, model.APIResponse{
            Error: &model.APIError{Code: "UPDATE_FAILED", Message: err.Error()},
        })
        return
    }
    if user == nil {
        c.JSON(http.StatusNotFound, model.APIResponse{
            Error: &model.APIError{Code: "NOT_FOUND", Message: "Utilisateur introuvable"},
        })
        return
    }
    c.JSON(http.StatusOK, model.APIResponse{Success: true, Data: user})
}

// DELETE /users/:id
func (h *UserHandler) Delete(c *gin.Context) {
    err := h.repo.Delete(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusInternalServerError, model.APIResponse{
            Error: &model.APIError{Code: "DELETE_FAILED", Message: err.Error()},
        })
        return
    }
    c.JSON(http.StatusNoContent, nil) // 204 sans corps
}

Router avec middlewares

// router/router.go
package router

import (
    "myapi/handler"
    "myapi/middleware"
    "github.com/gin-gonic/gin"
)

func Setup(userHandler *handler.UserHandler) *gin.Engine {
    r := gin.New()

    // Middlewares globaux
    r.Use(gin.Logger())
    r.Use(gin.Recovery())
    r.Use(middleware.CORS())
    r.Use(middleware.RateLimit(100)) // 100 req/min

    // Versioning de l'API
    v1 := r.Group("/api/v1")

    // Routes publiques
    v1.POST("/auth/login", handler.Login)
    v1.POST("/auth/register", handler.Register)

    // Routes protégées
    protected := v1.Group("")
    protected.Use(middleware.JWT())
    {
        users := protected.Group("/users")
        users.GET("", userHandler.List)
        users.GET("/:id", userHandler.Get)
        users.POST("", userHandler.Create)
        users.PATCH("/:id", userHandler.Update)
        users.DELETE("/:id", userHandler.Delete)

        // Routes admin uniquement
        admin := protected.Group("/admin")
        admin.Use(middleware.RequireRole("admin"))
        admin.GET("/stats", handler.Stats)
    }

    return r
}

Middleware JWT

// middleware/auth.go
package middleware

import (
    "net/http"
    "strings"
    "myapi/model"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

func JWT() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, model.APIResponse{
                Error: &model.APIError{Code: "MISSING_TOKEN", Message: "Token JWT requis"},
            })
            return
        }

        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, model.APIResponse{
                Error: &model.APIError{Code: "INVALID_FORMAT", Message: "Format: Bearer <token>"},
            })
            return
        }

        token, err := jwt.ParseWithClaims(parts[1], &jwt.MapClaims{},
            func(t *jwt.Token) (interface{}, error) {
                return []byte("votre_secret_jwt"), nil
            },
        )
        if err != nil || !token.Valid {
            c.AbortWithStatusJSON(http.StatusUnauthorized, model.APIResponse{
                Error: &model.APIError{Code: "INVALID_TOKEN", Message: "Token invalide ou expiré"},
            })
            return
        }

        claims := token.Claims.(*jwt.MapClaims)
        c.Set("user_id", (*claims)["sub"])
        c.Set("user_role", (*claims)["role"])
        c.Next()
    }
}

Implémenter une API REST en Python avec FastAPI

FastAPI est le framework Python moderne de référence pour les APIs REST. Typage automatique, documentation Swagger générée, validation Pydantic intégrée.
# Installation
pip install fastapi uvicorn sqlalchemy pydantic python-jose
# main.py — API REST complète avec FastAPI
from fastapi import FastAPI, HTTPException, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr
from typing import Optional, List
from datetime import datetime

app = FastAPI(
    title="Mon API REST",
    description="API REST complète avec FastAPI",
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "https://moussagaye.com"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# ── Modèles Pydantic ──────────────────────────────────────────
class UserCreate(BaseModel):
    email: EmailStr
    name: str
    password: str

    class Config:
        min_anystr_length = 2

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    name: Optional[str] = None

class UserResponse(BaseModel):
    id: str
    email: str
    name: str
    role: str
    created_at: datetime

class APIResponse(BaseModel):
    success: bool
    data: Optional[dict | list] = None
    error: Optional[dict] = None
    meta: Optional[dict] = None

# ── Endpoints ─────────────────────────────────────────────────
@app.get("/api/v1/users", response_model=APIResponse)
async def list_users(
    page: int = Query(1, ge=1),
    per_page: int = Query(20, ge=1, le=100),
    search: Optional[str] = None,
    db=Depends(get_db),
):
    query = db.query(User)
    if search:
        query = query.filter(
            User.name.ilike(f"%{search}%") |
            User.email.ilike(f"%{search}%")
        )

    total = query.count()
    users = query.offset((page - 1) * per_page).limit(per_page).all()

    return APIResponse(
        success=True,
        data=[u.to_dict() for u in users],
        meta={
            "page": page, "per_page": per_page,
            "total": total, "total_pages": (total + per_page - 1) // per_page,
        }
    )

@app.get("/api/v1/users/{user_id}", response_model=APIResponse)
async def get_user(user_id: str, db=Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(
            status_code=404,
            detail={"code": "NOT_FOUND", "message": "Utilisateur introuvable"}
        )
    return APIResponse(success=True, data=user.to_dict())

@app.post("/api/v1/users", response_model=APIResponse, status_code=201)
async def create_user(body: UserCreate, db=Depends(get_db)):
    existing = db.query(User).filter(User.email == body.email).first()
    if existing:
        raise HTTPException(
            status_code=409,
            detail={"code": "EMAIL_TAKEN", "message": "Email déjà utilisé"}
        )

    user = User(
        id=str(uuid4()),
        email=body.email,
        name=body.name,
        role="standard",
        password_hash=hash_password(body.password),
    )
    db.add(user)
    db.commit()
    db.refresh(user)

    return APIResponse(success=True, data=user.to_dict())

@app.patch("/api/v1/users/{user_id}", response_model=APIResponse)
async def update_user(user_id: str, body: UserUpdate, db=Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail={"code": "NOT_FOUND"})

    update_data = body.model_dump(exclude_none=True)
    for field, value in update_data.items():
        setattr(user, field, value)

    db.commit()
    db.refresh(user)
    return APIResponse(success=True, data=user.to_dict())

@app.delete("/api/v1/users/{user_id}", status_code=204)
async def delete_user(user_id: str, db=Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail={"code": "NOT_FOUND"})
    db.delete(user)
    db.commit()

Consommer une API REST depuis Next.js

Du côté frontend React/Next.js, voici comment consommer une API REST de façon propre avec gestion des erreurs, loading states et typage TypeScript.
// lib/api-client.ts — Client HTTP centralisé
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api/v1';

interface ApiOptions extends RequestInit {
  token?: string;
}

async function apiFetch<T>(endpoint: string, options: ApiOptions = {}): Promise<T> {
  const { token, ...fetchOptions } = options;

  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    ...(token && { Authorization: `Bearer ${token}` }),
    ...options.headers,
  };

  const res = await fetch(`${BASE_URL}${endpoint}`, {
    ...fetchOptions,
    headers,
  });

  const data = await res.json();

  if (!res.ok) {
    throw new ApiError(res.status, data.error?.code, data.error?.message);
  }

  return data.data as T;
}

class ApiError extends Error {
  constructor(
    public status: number,
    public code: string,
    message: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// API Users
export const usersApi = {
  list: (params?: { page?: number; search?: string }) =>
    apiFetch<User[]>(`/users?${new URLSearchParams(params as any)}`),

  get: (id: string) =>
    apiFetch<User>(`/users/${id}`),

  create: (data: CreateUserDto) =>
    apiFetch<User>('/users', { method: 'POST', body: JSON.stringify(data) }),

  update: (id: string, data: UpdateUserDto) =>
    apiFetch<User>(`/users/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),

  delete: (id: string) =>
    apiFetch<void>(`/users/${id}`, { method: 'DELETE' }),
};
// hooks/useUsers.ts — Hook React avec SWR
'use client';
import useSWR, { mutate } from 'swr';
import { usersApi } from '@/lib/api-client';

export function useUsers(page = 1, search?: string) {
  const key = `/users?page=${page}${search ? `&search=${search}` : ''}`;

  const { data, error, isLoading } = useSWR(key, () =>
    usersApi.list({ page, search })
  );

  const createUser = async (userData: CreateUserDto) => {
    const newUser = await usersApi.create(userData);
    mutate(key); // revalider la liste
    return newUser;
  };

  const deleteUser = async (id: string) => {
    await usersApi.delete(id);
    mutate(key);
  };

  return {
    users: data ?? [],
    isLoading,
    error: error?.message,
    createUser,
    deleteUser,
  };
}

Bonnes pratiques API REST — Checklist

Sécurité

  • Toujours valider les inputs côté serveur (pas seulement côté client)
  • Utiliser HTTPS en production — jamais HTTP
  • JWT avec expiration courte (15min) + refresh token longue durée
  • Rate limiting sur toutes les routes publiques (100 req/min par défaut)
  • CORS strict : whitelist explicite des origines autorisées
  • Sanitiser les inputs pour éviter les injections SQL et XSS
  • Headers de sécurité : X-Content-Type-Options, X-Frame-Options

Performance

  • Pagination systématique sur les listes (jamais de résultats illimités)
  • Cache HTTP via Cache-Control et ETag pour les ressources stables
  • Compression gzip/brotli sur les réponses
  • Index de base de données sur les champs filtrés et triés
  • Connection pooling pour la base de données
  • Requêtes N+1 : précharger les relations nécessaires

Documentation

  • Swagger/OpenAPI : documentation auto-générée (FastAPI le fait nativement)
  • Exemples de requêtes et réponses pour chaque endpoint
  • Codes d'erreur documentés avec actions correctives
  • Versioning explicite dans l'URL (/api/v1/)
  • Changelog des breaking changes
Une bonne API REST est comme une bonne interface publique : stable, prévisible, bien documentée. Vos consommateurs (front-end, mobile, partenaires) comptent sur cette stabilité.

REST vs GraphQL vs gRPC — Quand utiliser quoi ?

  • REST : API publique, applications CRUD classiques, interopérabilité maximale, cas d'usage général. Le choix par défaut.
  • GraphQL : Quand le client a besoin de flexibilité dans les champs qu'il récupère, applications mobiles avec bande passante limitée, BFF (Backend For Frontend) complexes.
  • gRPC : Communication inter-services en interne (microservices), besoin de performances maximales, streaming bidirectionnel, contrats stricts entre équipes.

Moussa Gaye

Développeur web freelance — France, Maroc, Sénégal

Travailler ensemble →