Tous les articlesTravailler ensemble →
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 actionsLes 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.goModè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