Développement Web

Créer une API REST avec Laravel 11 — tutoriel pas à pas

11 min de lecture

📍 Guide de référence de cette série : Laravel 11 et PHP 8.4 : installer l’environnement et maîtriser la nouvelle architecture

Une API REST bien conçue est le socle de n’importe quel projet moderne : applications mobiles, SPAs, intégrations tierces, microservices — tous consomment des endpoints HTTP structurés. Laravel 11 rend la création d’API particulièrement propre grâce à ses routes opt-in, ses contrôleurs ressource, ses Form Requests pour la validation et ses API Resources pour sérialiser les réponses JSON. Ce tutoriel construit une API complète pour la gestion d’articles de blog, étape par étape, en couvrant chaque couche de la pile.

Prérequis

  • PHP 8.2 minimum (8.4 recommandé) — voir le guide d’installation
  • Laravel 11 installé (composer create-project laravel/laravel mon-api "11.*")
  • Une base de données configurée (MySQL, PostgreSQL ou SQLite pour les tests)
  • Postman, Insomnia ou curl pour tester les endpoints
  • Temps estimé : 60 à 90 minutes

Étape 1 — Activer le routage API dans Laravel 11

Dans Laravel 11, contrairement aux versions précédentes, le fichier routes/api.php n’est pas créé par défaut. Cette décision de l’équipe Laravel part du principe que toutes les applications ne sont pas des APIs — inutile de générer du boilerplate qui ne servira pas. Pour activer explicitement le routing API et installer Laravel Sanctum (authentification par token) en même temps, une seule commande suffit :

php artisan install:api

Cette commande fait trois choses simultanément : elle crée le fichier routes/api.php avec un exemple de route protégée, installe Laravel Sanctum via Composer, publie les migrations Sanctum et les exécute. Si vous regardez bootstrap/app.php après, vous verrez qu’une entrée api: __DIR__.'/../routes/api.php' a été automatiquement ajoutée dans withRouting(). Toutes les routes définies dans routes/api.php reçoivent automatiquement le préfixe /api et le middleware api (stateless, sans sessions). Pour vérifier que le routing est en place :

php artisan route:list

Vous devez voir au minimum la route GET /api/user générée par Sanctum. Si cette route apparaît, le routing API est correctement activé.

Étape 2 — Créer le modèle, la migration et le contrôleur

Laravel encourage la cohérence entre le modèle de données, la migration SQL et le contrôleur métier. La commande make:model avec les bons flags génère tout en une fois, ce qui évite d’oublier l’un des fichiers. Pour notre API d’articles de blog :

# Génère le modèle Article, sa migration, sa factory, son seeder et son contrôleur API
php artisan make:model Article -msc --api

Cette commande crée quatre fichiers : app/Models/Article.php, une migration dans database/migrations/, database/seeders/ArticleSeeder.php, et app/Http/Controllers/ArticleController.php avec les méthodes index, store, show, update et destroy (sans create ni edit — inutiles pour une API REST). Ouvrez la migration générée et définissez le schéma :

<?php
// database/migrations/xxxx_create_articles_table.php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->string('slug')->unique();
            $table->enum('status', ['draft', 'published'])->default('draft');
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('articles');
    }
};

Exécutez ensuite la migration pour créer la table en base :

php artisan migrate

Le résultat attendu est un message INFO Running migrations. suivi de create_articles_table .............. Xms DONE. Si vous voyez une erreur de connexion à la base de données, vérifiez votre fichier .env — les variables DB_HOST, DB_DATABASE, DB_USERNAME et DB_PASSWORD doivent pointer vers une base accessible.

Étape 3 — Configurer le modèle Article

Le modèle Eloquent est le point d’entrée pour toutes les opérations sur la table articles. Il faut déclarer les colonnes autorisées en écriture ($fillable), les castings de types et la relation avec le modèle User. La protection par masse d’affectation ($fillable) est un garde-fou de sécurité essentiel : elle empêche qu’un champ non prévu (comme user_id ou status) soit modifié directement depuis les données de la requête HTTP sans contrôle explicite.

<?php
// app/Models/Article.php

namespace AppModels;

use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsBelongsTo;
use IlluminateSupportStr;

class Article extends Model
{
    protected $fillable = [
        'title', 'content', 'slug', 'status', 'published_at'
    ];

    protected $casts = [
        'published_at' => 'datetime',
    ];

    // Relation : un article appartient à un utilisateur
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    // Génération automatique du slug à partir du titre
    protected static function booted(): void
    {
        static::creating(function (Article $article) {
            if (empty($article->slug)) {
                $article->slug = Str::slug($article->title);
            }
        });
    }
}

Le hook booted() avec creating est un observer inline : à chaque création d’article, si le champ slug n’est pas fourni, Laravel génère automatiquement un slug à partir du titre (Str::slug('Mon Article') donne mon-article). Ce comportement évite de dupliquer la logique de génération de slug dans chaque contrôleur ou service.

Important : Pour que $user->articles()->create(...) fonctionne dans le contrôleur (Étape 5), il faut aussi ajouter la relation inverse hasMany dans le modèle User :

<?php
// app/Models/User.php — ajouter la relation articles()

use IlluminateDatabaseEloquentRelationsHasMany;

class User extends Authenticatable
{
    // ... HasApiTokens, HasFactory, Notifiable

    public function articles(): HasMany
    {
        return $this->hasMany(Article::class);
    }
}

Cette relation permet à Laravel d’injecter automatiquement user_id lors de la création depuis le contrôleur — sans avoir à le passer manuellement.

Étape 4 — Valider les données avec les Form Requests

Dans une API REST Laravel, la validation des données entrantes ne se fait pas directement dans le contrôleur — c’est le rôle des Form Requests. Cette séparation de responsabilités garde le contrôleur propre et centralise les règles de validation dans des classes dédiées, testables indépendamment. Créez les deux Form Requests dont vous avez besoin :

php artisan make:request StoreArticleRequest
php artisan make:request UpdateArticleRequest
<?php
// app/Http/Requests/StoreArticleRequest.php

namespace AppHttpRequests;

use IlluminateFoundationHttpFormRequest;

class StoreArticleRequest extends FormRequest
{
    public function authorize(): bool
    {
        // Retourne true si l'utilisateur est authentifié (via Sanctum)
        return $this->user() !== null;
    }

    public function rules(): array
    {
        return [
            'title'        => ['required', 'string', 'min:5', 'max:255'],
            'content'      => ['required', 'string', 'min:50'],
            'slug'         => ['nullable', 'string', 'unique:articles,slug'],
            'status'       => ['in:draft,published'],
            'published_at' => ['nullable', 'date', 'required_if:status,published'],
        ];
    }
}

Laravel applique ces règles automatiquement avant d’entrer dans la méthode du contrôleur. Si la validation échoue, Laravel retourne une réponse JSON 422 Unprocessable Content avec le détail des erreurs par champ — exactement ce qu’un client API attend. Le champ authorize() est également évalué en amont : si vous retournez false, Laravel répond 403 Forbidden avant même d’exécuter les règles de validation.

Étape 5 — Implémenter le contrôleur API

Le contrôleur reçoit les requêtes validées, interroge le modèle Eloquent et retourne des réponses JSON structurées. En API REST Laravel, il est recommandé d’utiliser les API Resources (couche de présentation) plutôt que de retourner les modèles directement — cela protège votre contrat d’API des changements de schéma interne.

<?php
// app/Http/Controllers/ArticleController.php

namespace AppHttpControllers;

use AppHttpRequestsStoreArticleRequest;
use AppHttpRequestsUpdateArticleRequest;
use AppHttpResourcesArticleResource;
use AppModelsArticle;
use IlluminateHttpJsonResponse;
use IlluminateHttpResourcesJsonAnonymousResourceCollection;

class ArticleController extends Controller
{
    // GET /api/articles — liste paginée
    public function index(): AnonymousResourceCollection
    {
        $articles = Article::with('user')
            ->latest()
            ->paginate(15);

        return ArticleResource::collection($articles);
    }

    // POST /api/articles — création
    public function store(StoreArticleRequest $request): ArticleResource
    {
        $article = $request->user()->articles()->create($request->validated());

        return new ArticleResource($article);
    }

    // GET /api/articles/{article} — lecture
    public function show(Article $article): ArticleResource
    {
        return new ArticleResource($article->load('user'));
    }

    // PUT /api/articles/{article} — mise à jour
    public function update(UpdateArticleRequest $request, Article $article): ArticleResource
    {
        $article->update($request->validated());

        return new ArticleResource($article);
    }

    // DELETE /api/articles/{article} — suppression
    public function destroy(Article $article): JsonResponse
    {
        $article->delete();

        return response()->json(null, 204);
    }
}

La méthode show(Article $article) utilise le Route Model Binding de Laravel : quand une route reçoit le paramètre {article}, Laravel interroge automatiquement la base pour trouver l’article par son ID et injecte l’instance dans le contrôleur. Si l’article n’existe pas, Laravel retourne automatiquement une réponse 404 Not Found sans que vous ayez à écrire cette logique. C’est un gain de sécurité et de lisibilité considérable.

Étape 6 — Créer l’API Resource pour sérialiser les réponses

Les API Resources sont des classes de transformation qui contrôlent exactement quels champs sont exposés dans la réponse JSON. Elles évitent l’exposition accidentelle de données sensibles (hash de mot de passe, token interne) et permettent de versionner votre API sans modifier les modèles. Créez la resource pour Article :

php artisan make:resource ArticleResource
<?php
// app/Http/Resources/ArticleResource.php

namespace AppHttpResources;

use IlluminateHttpRequest;
use IlluminateHttpResourcesJsonJsonResource;

class ArticleResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'           => $this->id,
            'title'        => $this->title,
            'slug'         => $this->slug,
            'content'      => $this->content,
            'status'       => $this->status,
            'published_at' => $this->published_at?->toIso8601String(),
            'created_at'   => $this->created_at->toIso8601String(),
            'author'       => [
                'id'   => $this->user?->id,
                'name' => $this->user?->name,
            ],
        ];
    }
}

La syntaxe $this->published_at?->toIso8601String() utilise l’opérateur nullsafe de PHP : si published_at est null, l’expression retourne null sans lever d’exception. Le cast 'datetime' dans le modèle garantit que le champ est automatiquement converti en objet Carbon (qui propose toIso8601String()) lors de la récupération depuis la base de données.

Étape 7 — Déclarer les routes et tester l’API

Ouvrez routes/api.php et déclarez les routes ressource pour les articles. La méthode Route::apiResource génère automatiquement les cinq routes REST standards (index, store, show, update, destroy) avec les bons verbes HTTP (GET, POST, PUT, DELETE) et les bons noms de routes :

<?php
// routes/api.php

use AppHttpControllersArticleController;
use IlluminateSupportFacadesRoute;

// Routes publiques
Route::apiResource('articles', ArticleController::class)->only(['index', 'show']);

// Routes protégées (requièrent un token Sanctum)
Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('articles', ArticleController::class)->except(['index', 'show']);
});

Listez les routes pour confirmer leur enregistrement :

php artisan route:list --path=api/articles

Vous devez voir cinq routes : GET /api/articles, POST /api/articles, GET /api/articles/{article}, PUT /api/articles/{article}, DELETE /api/articles/{article}. Testez l’endpoint public avec curl :

curl -s http://localhost:8000/api/articles | jq .

La réponse doit être un objet JSON avec une clé data (tableau des articles) et une clé meta contenant les informations de pagination (current_page, total, per_page). Si la réponse est {"data":[]}, la table est vide — exécutez php artisan db:seed --class=ArticleSeeder pour la remplir.

Erreurs fréquentes

Erreur Cause Solution
404 sur /api/articles routes/api.php absent (oubli de php artisan install:api) Lancer php artisan install:api puis vérifier php artisan route:list
422 sur POST même avec des données valides authorize() retourne false dans le Form Request Retourner true pour les routes publiques ou vérifier l’authentification
Champ user_id non rempli automatiquement Utilisation de Article::create() au lieu de $request->user()->articles()->create() Toujours créer via la relation pour que l’ID de l’utilisateur soit injecté
Réponse JSON contient des champs sensibles Retour direct du modèle au lieu d’une API Resource Wraper toutes les réponses dans ArticleResource ou ArticleResource::collection()

FAQ

Doit-on toujours utiliser les API Resources ?
Oui, en production. Retourner le modèle directement (return $article) expose tous les attributs, y compris les éventuels champs sensibles ajoutés ultérieurement. Les Resources découplent le contrat d’API du schéma de base de données.

Comment versionner l’API ?
En préfixant les routes : Route::prefix('v1')->group(fn() => Route::apiResource(...)). Chaque version peut avoir ses propres Resources et Controllers dans des sous-dossiers dédiés.

Quelle différence entre apiResource et resource ?
resource génère 7 routes (avec create et edit qui retournent des formulaires HTML). apiResource génère 5 routes seulement — sans les routes de formulaires, inutiles pour une API REST.

Tutoriels frères dans cette série

Pour aller plus loin

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité