Construire des API REST avec Go et Fiber : Guide pratique pour débutants

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Go est le langage derrière Docker, Kubernetes et la majorité de l'infrastructure cloud-native — et Fiber le rend accessible aux développeurs web. Inspiré par Express.js et construit sur Fasthttp, Fiber offre jusqu'à 10 fois le débit de Node.js avec la sécurité mémoire et la concurrence basée sur les goroutines. Dans ce tutoriel, vous construirez une API REST complète de zéro.

Ce que vous allez construire

Une API de gestion de tâches (Task Manager) complète avec :

  • Opérations CRUD complètes pour les tâches (créer, lire, mettre à jour, supprimer)
  • Traitement des requêtes et réponses JSON
  • Base de données PostgreSQL avec GORM (l'ORM le plus populaire de Go)
  • Gestion structurée des erreurs avec les codes HTTP appropriés
  • Middlewares (journalisation, CORS, limitation de débit)
  • Validation des entrées
  • Configuration basée sur les variables d'environnement
  • Tests d'intégration

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Go 1.22+ installé via go.dev/dl
  • PostgreSQL 15+ en local ou via Docker
  • Une compréhension de base du protocole HTTP et des concepts REST
  • Un éditeur de code (VS Code avec l'extension Go recommandé)
  • Un terminal

Nouveau sur Go ? Vous devez être à l'aise avec les structs, les interfaces, la gestion des erreurs et les goroutines. Le Tour of Go officiel est un excellent point de départ.


Pourquoi Fiber ?

Avant de plonger dans le code, comprenons pourquoi Fiber se démarque dans l'écosystème Go :

CaractéristiqueFiberGinEchoChi
Inspiré parExpress.jsMartiniSinatrastdlib
Moteur HTTPFasthttpnet/httpnet/httpnet/http
PerformanceLe plus rapideRapideRapideRapide
Courbe d'apprentissageFaible (style Express)FaibleModéréeFaible
MiddlewaresIntégrés + personnalisésCommunautéIntégrésCompatibles stdlib

L'API de Fiber, similaire à Express, signifie que si vous avez déjà construit des serveurs Node.js, vous vous sentirez chez vous — mais avec les performances et la sécurité des types de Go.


Étape 1 : Configuration du projet

Créez le répertoire du projet et initialisez le module Go :

mkdir task-api && cd task-api
go mod init github.com/yourusername/task-api

Installez les dépendances principales :

go get github.com/gofiber/fiber/v2
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/joho/godotenv

Créez la structure du projet :

mkdir -p cmd/api internal/{models,handlers,database,middleware,config}
touch cmd/api/main.go
touch .env

Votre projet devrait ressembler à ceci :

task-api/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── config/
│   │   └── config.go
│   ├── database/
│   │   └── database.go
│   ├── handlers/
│   │   └── task.go
│   ├── middleware/
│   │   └── middleware.go
│   └── models/
│       └── task.go
├── .env
├── go.mod
└── go.sum

Étape 2 : Configuration de l'environnement

Configurez votre fichier .env :

DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=taskapi
APP_PORT=3000

Créez le chargeur de configuration dans internal/config/config.go :

package config
 
import (
	"log"
	"os"
 
	"github.com/joho/godotenv"
)
 
type Config struct {
	DBHost     string
	DBPort     string
	DBUser     string
	DBPassword string
	DBName     string
	AppPort    string
}
 
func LoadConfig() *Config {
	err := godotenv.Load()
	if err != nil {
		log.Println("No .env file found, using system environment variables")
	}
 
	return &Config{
		DBHost:     getEnv("DB_HOST", "localhost"),
		DBPort:     getEnv("DB_PORT", "5432"),
		DBUser:     getEnv("DB_USER", "postgres"),
		DBPassword: getEnv("DB_PASSWORD", "postgres"),
		DBName:     getEnv("DB_NAME", "taskapi"),
		AppPort:    getEnv("APP_PORT", "3000"),
	}
}
 
func getEnv(key, fallback string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return fallback
}

Étape 3 : Définir le modèle de données

Créez le modèle Task dans internal/models/task.go :

package models
 
import (
	"time"
 
	"gorm.io/gorm"
)
 
type TaskStatus string
 
const (
	StatusPending    TaskStatus = "pending"
	StatusInProgress TaskStatus = "in_progress"
	StatusCompleted  TaskStatus = "completed"
)
 
type Task struct {
	ID          uint           `json:"id" gorm:"primaryKey"`
	Title       string         `json:"title" gorm:"size:255;not null"`
	Description string         `json:"description" gorm:"type:text"`
	Status      TaskStatus     `json:"status" gorm:"size:20;default:pending"`
	Priority    int            `json:"priority" gorm:"default:0"`
	DueDate     *time.Time     `json:"due_date,omitempty"`
	CreatedAt   time.Time      `json:"created_at"`
	UpdatedAt   time.Time      `json:"updated_at"`
	DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
}
 
type CreateTaskRequest struct {
	Title       string     `json:"title"`
	Description string     `json:"description"`
	Priority    int        `json:"priority"`
	DueDate     *time.Time `json:"due_date"`
}
 
type UpdateTaskRequest struct {
	Title       *string     `json:"title"`
	Description *string     `json:"description"`
	Status      *TaskStatus `json:"status"`
	Priority    *int        `json:"priority"`
	DueDate     *time.Time  `json:"due_date"`
}
 
func (r *CreateTaskRequest) Validate() map[string]string {
	errors := make(map[string]string)
	if r.Title == "" {
		errors["title"] = "Title is required"
	}
	if len(r.Title) > 255 {
		errors["title"] = "Title must be 255 characters or less"
	}
	if r.Priority < 0 || r.Priority > 5 {
		errors["priority"] = "Priority must be between 0 and 5"
	}
	return errors
}

Remarquez comment nous utilisons gorm.DeletedAt pour la suppression douce — les enregistrements ne seront pas définitivement supprimés de la base de données, ce qui facilite leur restauration ultérieure.


Étape 4 : Connexion à la base de données

Configurez la couche base de données dans internal/database/database.go :

package database
 
import (
	"fmt"
	"log"
 
	"github.com/yourusername/task-api/internal/config"
	"github.com/yourusername/task-api/internal/models"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)
 
var DB *gorm.DB
 
func Connect(cfg *config.Config) {
	dsn := fmt.Sprintf(
		"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
		cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName,
	)
 
	var err error
	DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
	}
 
	log.Println("Database connected successfully")
 
	err = DB.AutoMigrate(&models.Task{})
	if err != nil {
		log.Fatalf("Failed to migrate database: %v", err)
	}
 
	log.Println("Database migrated successfully")
}

Créez la base de données PostgreSQL avant de lancer l'application :

createdb taskapi
# Ou avec psql :
# psql -U postgres -c "CREATE DATABASE taskapi;"

Étape 5 : Construire les handlers de l'API

C'est le cœur de votre API. Créez internal/handlers/task.go :

package handlers
 
import (
	"strconv"
 
	"github.com/gofiber/fiber/v2"
	"github.com/yourusername/task-api/internal/database"
	"github.com/yourusername/task-api/internal/models"
)
 
// ListTasks retourne toutes les tâches avec filtrage optionnel
func ListTasks(c *fiber.Ctx) error {
	var tasks []models.Task
 
	query := database.DB
 
	// Filtrer par statut si fourni
	if status := c.Query("status"); status != "" {
		query = query.Where("status = ?", status)
	}
 
	// Filtrer par priorité si fourni
	if priority := c.Query("priority"); priority != "" {
		query = query.Where("priority = ?", priority)
	}
 
	// Pagination
	page, _ := strconv.Atoi(c.Query("page", "1"))
	limit, _ := strconv.Atoi(c.Query("limit", "10"))
	offset := (page - 1) * limit
 
	var total int64
	query.Model(&models.Task{}).Count(&total)
 
	result := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&tasks)
	if result.Error != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": "Failed to fetch tasks",
		})
	}
 
	return c.JSON(fiber.Map{
		"data":  tasks,
		"total": total,
		"page":  page,
		"limit": limit,
	})
}
 
// GetTask retourne une seule tâche par ID
func GetTask(c *fiber.Ctx) error {
	id := c.Params("id")
 
	var task models.Task
	result := database.DB.First(&task, id)
	if result.Error != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": "Task not found",
		})
	}
 
	return c.JSON(task)
}
 
// CreateTask crée une nouvelle tâche
func CreateTask(c *fiber.Ctx) error {
	req := new(models.CreateTaskRequest)
 
	if err := c.BodyParser(req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid request body",
		})
	}
 
	// Validation
	if errors := req.Validate(); len(errors) > 0 {
		return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
			"errors": errors,
		})
	}
 
	task := models.Task{
		Title:       req.Title,
		Description: req.Description,
		Priority:    req.Priority,
		DueDate:     req.DueDate,
		Status:      models.StatusPending,
	}
 
	result := database.DB.Create(&task)
	if result.Error != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": "Failed to create task",
		})
	}
 
	return c.Status(fiber.StatusCreated).JSON(task)
}
 
// UpdateTask met à jour une tâche existante
func UpdateTask(c *fiber.Ctx) error {
	id := c.Params("id")
 
	var task models.Task
	if err := database.DB.First(&task, id).Error; err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": "Task not found",
		})
	}
 
	req := new(models.UpdateTaskRequest)
	if err := c.BodyParser(req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid request body",
		})
	}
 
	// Appliquer les mises à jour partielles
	if req.Title != nil {
		task.Title = *req.Title
	}
	if req.Description != nil {
		task.Description = *req.Description
	}
	if req.Status != nil {
		task.Status = *req.Status
	}
	if req.Priority != nil {
		task.Priority = *req.Priority
	}
	if req.DueDate != nil {
		task.DueDate = req.DueDate
	}
 
	database.DB.Save(&task)
 
	return c.JSON(task)
}
 
// DeleteTask supprime une tâche (suppression douce)
func DeleteTask(c *fiber.Ctx) error {
	id := c.Params("id")
 
	var task models.Task
	if err := database.DB.First(&task, id).Error; err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": "Task not found",
		})
	}
 
	database.DB.Delete(&task)
 
	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"message": "Task deleted successfully",
	})
}

Étape 6 : Ajouter les middlewares

Créez internal/middleware/middleware.go pour la journalisation, CORS et la limitation de débit :

package middleware
 
import (
	"log"
	"time"
 
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/gofiber/fiber/v2/middleware/limiter"
)
 
func SetupMiddleware(app *fiber.App) {
	// CORS
	app.Use(cors.New(cors.Config{
		AllowOrigins: "*",
		AllowMethods: "GET,POST,PUT,PATCH,DELETE",
		AllowHeaders: "Origin,Content-Type,Accept,Authorization",
	}))
 
	// Limitation de débit : 100 requêtes par minute
	app.Use(limiter.New(limiter.Config{
		Max:               100,
		Expiration:        1 * time.Minute,
		LimiterMiddleware: limiter.SlidingWindow{},
	}))
 
	// Journalisation des requêtes
	app.Use(func(c *fiber.Ctx) error {
		start := time.Now()
		err := c.Next()
		duration := time.Since(start)
 
		log.Printf("%s %s %d %s",
			c.Method(),
			c.Path(),
			c.Response().StatusCode(),
			duration,
		)
 
		return err
	})
}

Étape 7 : Assembler le tout

Créez le point d'entrée dans cmd/api/main.go :

package main
 
import (
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
 
	"github.com/gofiber/fiber/v2"
	"github.com/yourusername/task-api/internal/config"
	"github.com/yourusername/task-api/internal/database"
	"github.com/yourusername/task-api/internal/handlers"
	"github.com/yourusername/task-api/internal/middleware"
)
 
func main() {
	// Charger la configuration
	cfg := config.LoadConfig()
 
	// Se connecter à la base de données
	database.Connect(cfg)
 
	// Créer l'application Fiber
	app := fiber.New(fiber.Config{
		AppName:      "Task API v1.0",
		ErrorHandler: customErrorHandler,
	})
 
	// Configurer les middlewares
	middleware.SetupMiddleware(app)
 
	// Vérification de santé
	app.Get("/health", func(c *fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"status":  "ok",
			"service": "task-api",
		})
	})
 
	// Routes API v1
	v1 := app.Group("/api/v1")
	{
		tasks := v1.Group("/tasks")
		tasks.Get("/", handlers.ListTasks)
		tasks.Get("/:id", handlers.GetTask)
		tasks.Post("/", handlers.CreateTask)
		tasks.Put("/:id", handlers.UpdateTask)
		tasks.Delete("/:id", handlers.DeleteTask)
	}
 
	// Arrêt gracieux
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 
	go func() {
		<-quit
		log.Println("Shutting down server...")
		if err := app.Shutdown(); err != nil {
			log.Fatalf("Server shutdown failed: %v", err)
		}
	}()
 
	// Démarrer le serveur
	addr := fmt.Sprintf(":%s", cfg.AppPort)
	log.Printf("Server starting on %s", addr)
	log.Fatal(app.Listen(addr))
}
 
func customErrorHandler(c *fiber.Ctx, err error) error {
	code := fiber.StatusInternalServerError
	if e, ok := err.(*fiber.Error); ok {
		code = e.Code
	}
	return c.Status(code).JSON(fiber.Map{
		"error": err.Error(),
	})
}

Étape 8 : Lancer et tester votre API

Démarrez le serveur :

go run cmd/api/main.go

Vous devriez voir :

Database connected successfully
Database migrated successfully
Server starting on :3000

Testez maintenant avec curl :

Créer une tâche :

curl -X POST http://localhost:3000/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Apprendre Go Fiber",
    "description": "Terminer le tutoriel REST API",
    "priority": 3
  }'

Réponse :

{
  "id": 1,
  "title": "Apprendre Go Fiber",
  "description": "Terminer le tutoriel REST API",
  "status": "pending",
  "priority": 3,
  "created_at": "2026-03-02T10:00:00Z",
  "updated_at": "2026-03-02T10:00:00Z"
}

Lister toutes les tâches :

curl http://localhost:3000/api/v1/tasks

Mettre à jour une tâche :

curl -X PUT http://localhost:3000/api/v1/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "in_progress"}'

Filtrer par statut :

curl "http://localhost:3000/api/v1/tasks?status=pending&page=1&limit=5"

Supprimer une tâche :

curl -X DELETE http://localhost:3000/api/v1/tasks/1

Étape 9 : Écrire les tests d'intégration

Créez cmd/api/main_test.go :

package main
 
import (
	"bytes"
	"encoding/json"
	"net/http/httptest"
	"testing"
 
	"github.com/gofiber/fiber/v2"
	"github.com/yourusername/task-api/internal/handlers"
	"github.com/yourusername/task-api/internal/models"
)
 
func setupTestApp() *fiber.App {
	app := fiber.New()
 
	v1 := app.Group("/api/v1")
	tasks := v1.Group("/tasks")
	tasks.Get("/", handlers.ListTasks)
	tasks.Get("/:id", handlers.GetTask)
	tasks.Post("/", handlers.CreateTask)
	tasks.Put("/:id", handlers.UpdateTask)
	tasks.Delete("/:id", handlers.DeleteTask)
 
	return app
}
 
func TestCreateTask(t *testing.T) {
	app := setupTestApp()
 
	task := models.CreateTaskRequest{
		Title:       "Test Task",
		Description: "A task for testing",
		Priority:    2,
	}
 
	body, _ := json.Marshal(task)
	req := httptest.NewRequest("POST", "/api/v1/tasks", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
 
	resp, err := app.Test(req, -1)
	if err != nil {
		t.Fatalf("Failed to make request: %v", err)
	}
 
	if resp.StatusCode != fiber.StatusCreated {
		t.Errorf("Expected status 201, got %d", resp.StatusCode)
	}
}
 
func TestCreateTaskValidation(t *testing.T) {
	app := setupTestApp()
 
	task := models.CreateTaskRequest{
		Title: "",
	}
 
	body, _ := json.Marshal(task)
	req := httptest.NewRequest("POST", "/api/v1/tasks", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
 
	resp, err := app.Test(req, -1)
	if err != nil {
		t.Fatalf("Failed to make request: %v", err)
	}
 
	if resp.StatusCode != fiber.StatusUnprocessableEntity {
		t.Errorf("Expected status 422, got %d", resp.StatusCode)
	}
}
 
func TestGetNonExistentTask(t *testing.T) {
	app := setupTestApp()
 
	req := httptest.NewRequest("GET", "/api/v1/tasks/99999", nil)
 
	resp, err := app.Test(req, -1)
	if err != nil {
		t.Fatalf("Failed to make request: %v", err)
	}
 
	if resp.StatusCode != fiber.StatusNotFound {
		t.Errorf("Expected status 404, got %d", resp.StatusCode)
	}
}

Lancez les tests :

go test ./cmd/api/ -v

Dépannage

Erreur "connection refused" lors de la connexion à PostgreSQL : Assurez-vous que PostgreSQL est en cours d'exécution et que les identifiants dans .env correspondent. Si vous utilisez Docker :

docker run --name taskdb -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:15
docker exec -it taskdb psql -U postgres -c "CREATE DATABASE taskapi;"

Erreurs "go: module not found" : Exécutez go mod tidy pour résoudre les problèmes de dépendances.

Port déjà utilisé : Changez APP_PORT dans .env ou arrêtez le processus utilisant le port :

lsof -ti:3000 | xargs kill

Comparaison des performances

Voici comment Fiber se compare aux frameworks populaires à travers les langages :

FrameworkLangageReq/sec (hello world)Mémoire
FiberGo~500 000~10 Mo
GinGo~350 000~12 Mo
ExpressNode.js~40 000~80 Mo
FastAPIPython~25 000~50 Mo
AxumRust~550 000~8 Mo

Les benchmarks varient selon le matériel. Source : TechEmpower Framework Benchmarks.


Prochaines étapes

  • Ajouter l'authentification JWT avec github.com/gofiber/jwt/v3
  • Ajouter la documentation Swagger avec github.com/gofiber/swagger
  • Déployer avec Docker — créer un Dockerfile multi-étapes pour une image minimale
  • Ajouter le support WebSocket pour les mises à jour de tâches en temps réel
  • Implémenter le cache avec Redis via github.com/gofiber/storage/redis

Conclusion

Vous avez construit une API REST complète avec Go et Fiber qui comprend :

  • Une structure de projet propre suivant les bonnes pratiques de Go
  • Des opérations CRUD complètes avec PostgreSQL et GORM
  • La validation des entrées et une gestion structurée des erreurs
  • Des middlewares pour CORS, la limitation de débit et la journalisation
  • Un arrêt gracieux pour la préparation à la production
  • Des tests d'intégration utilisant les outils de test intégrés de Fiber

Go et Fiber vous donnent les performances d'un langage compilé avec l'expérience développeur d'Express.js. Le binaire résultant est un exécutable unique et autonome qui démarre en millisecondes et gère des milliers de connexions simultanées avec une consommation mémoire minimale.

Pour votre prochain projet, envisagez d'ajouter l'authentification, le support WebSocket, ou le déploiement sur une plateforme cloud avec Docker. Les fondations que vous avez construites ici s'adaptent des projets personnels aux charges de production servant des millions de requêtes.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Orchestrer les agents : Routines et transferts.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire des API REST avec Rust et Axum : guide pratique pour débutants

Apprenez à construire des API REST rapides et sûres avec Rust et le framework web Axum. Ce guide étape par étape couvre la configuration du projet, le routage, la gestion JSON, l'intégration base de données avec SQLx, la gestion des erreurs et les tests — de zéro à une API fonctionnelle.

30 min read·