🌐 API REST avec Symfony

Créer des APIs modernes et performantes

REST ‱ JSON ‱ HTTP ‱ Serialization ‱ JWT ‱ API Platform

Formation DWWM - 2026

Au programme 📋

# 🎯 Partie 1

Qu'est-ce qu'une API REST ?

Concepts fondamentaux

C'est quoi une API ? đŸ€”

API (Application Programming Interface) = Interface qui permet Ă  des applications de communiquer entre elles.

Analogie : Le serveur au restaurant đŸœïž

Une API = le serveur dans un restaurant :

Tu ne vas pas en cuisine toi-mĂȘme ! Tu passes par le serveur (API) qui transmet ta demande.

REST = REpresentational State Transfer 🌐

REST = Style d'architecture pour les APIs web, basé sur HTTP.

Principes REST

Principe Explication
Client-Serveur Séparation entre frontend (client) et backend (serveur)
Sans Ă©tat (Stateless) Chaque requĂȘte contient TOUTES les infos nĂ©cessaires
Cacheable Les rĂ©ponses peuvent ĂȘtre mises en cache
Interface uniforme Utilisation standard des méthodes HTTP (GET, POST...)
Ressources Tout est une ressource (article, user, product...)

Exemple concret d'API REST 💡

API de blog

Action Méthode HTTP URL Résultat
Lister les articles GET /api/articles Liste JSON des articles
Voir un article GET /api/articles/5 Article #5 en JSON
Créer un article POST /api/articles Nouvel article créé
Modifier un article PUT/PATCH /api/articles/5 Article #5 modifié
Supprimer un article DELETE /api/articles/5 Article #5 supprimé

✅ Avantages d'une API REST :

# 📡 Partie 2

HTTP et Méthodes REST

GET, POST, PUT, PATCH, DELETE

Les mĂ©thodes HTTP (verbes REST) 📡

Méthode Action CRUD Exemple Idempotent ?
GET Read (lire) GET /api/articles ✅ Oui
POST Create (crĂ©er) POST /api/articles ❌ Non
PUT Update (remplacer) PUT /api/articles/5 ✅ Oui
PATCH Update (modifier) PATCH /api/articles/5 ❌ Non
DELETE Delete (supprimer) DELETE /api/articles/5 ✅ Oui

💡 Idempotent :

Une requĂȘte est idempotente si l'appeler plusieurs fois produit le mĂȘme rĂ©sultat qu'une seule fois.

PUT vs PATCH 🆚

PUT - Remplacement complet

Remplace toute la ressource

PUT /api/articles/5

{
  "title": "Nouveau titre",
  "content": "Nouveau contenu",
  "author": "John Doe",
  "status": "published"
}

// Remplace TOUS les champs

Si tu oublies un champ, il sera mis Ă  null !

PATCH - Modification partielle

Modifie seulement certains champs

PATCH /api/articles/5

{
  "title": "Nouveau titre"
}

// Modifie SEULEMENT le titre
// Les autres champs inchangés

Plus pratique pour les mises Ă  jour partielles !

Codes de statut HTTP 📊

Codes de succĂšs (2xx)

Code Signification Usage
200 OK SuccÚs GET, PUT, PATCH réussis
201 Created Ressource créée POST réussi
204 No Content SuccÚs sans contenu DELETE réussi

Codes d'erreur client (4xx)

Code Signification Usage
400 Bad Request RequĂȘte invalide JSON mal formĂ©
401 Unauthorized Non authentifié Token manquant/invalide
403 Forbidden Interdit Pas les droits
404 Not Found Ressource introuvable Article #999 n'existe pas
422 Unprocessable Validation échouée Email invalide, champ manquant

Codes d'erreur serveur (5xx)

Code Signification Usage
500 Internal Error Erreur serveur Exception non gérée
503 Unavailable Service indisponible Maintenance
# 📩 Partie 3

JSON et Sérialisation

Serializer Component

JSON - JavaScript Object Notation 📩

JSON = Format d'échange de données standard pour les APIs REST.

Exemple de réponse JSON

{
  "id": 5,
  "title": "Introduction Ă  Symfony",
  "content": "Symfony est un framework PHP...",
  "author": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "tags": ["php", "symfony", "framework"],
  "published": true,
  "publishedAt": "2025-01-31T10:30:00+00:00",
  "commentsCount": 12
}

✅ Avantages de JSON :

SĂ©rialisation = Objet PHP → JSON 🔄

Analogie : L'emballage cadeau 🎁

Sérialisation = emballer un objet pour l'envoyer :

On ne peut pas envoyer directement un objet PHP sur HTTP. Il faut le transformer en JSON !

Symfony Serializer Component 🔧

Installation

composer require symfony/serializer

Utilisation basique

use Symfony\Component\Serializer\SerializerInterface;

#[Route('/api/articles/{id}', methods: ['GET'])]
public function show(
    Article $article,
    SerializerInterface $serializer
): JsonResponse {
    // Objet PHP → JSON
    $json = $serializer->serialize($article, 'json');
    
    return new JsonResponse($json, 200, [], true);
    
    // OU plus simple :
    return $this->json($article);
}

💡 $this->json() :

Raccourci d'AbstractController qui sérialise automatiquement en JSON !

Groupes de sĂ©rialisation 🎯

ContrÎler quels champs sont sérialisés avec les groupes.

Dans l'entité

use Symfony\Component\Serializer\Annotation\Groups;

class Article
{
    #[Groups(['article:read', 'article:write'])]
    private ?int $id = null;
    
    #[Groups(['article:read', 'article:write'])]
    private ?string $title = null;
    
    #[Groups(['article:read', 'article:write'])]
    private ?string $content = null;
    
    #[Groups(['article:read'])]  // Lecture seule
    private ?\DateTimeInterface $createdAt = null;
    
    // JAMAIS sérialisé (pas de groupe)
    private ?string $internalNotes = null;
}

Utiliser les groupes

return $this->json($article, 200, [], [
    'groups' => ['article:read']
]);

ProblĂšme : RĂ©fĂ©rences circulaires ♻

Entités avec relations

class Article
{
    private Author $author;  // Article → Author
}

class Author
{
    private Collection $articles;  // Author → Articles → Author...
}

// ProblĂšme : Boucle infinie !
return $this->json($article);  // ❌ ERREUR

⚠ Erreur :

A circular reference has been detected...

Solution : MaxDepth

use Symfony\Component\Serializer\Annotation\MaxDepth;

class Article
{
    #[MaxDepth(1)]  // Limite Ă  1 niveau
    private ?Author $author = null;
}

// Dans le contrĂŽleur
return $this->json($article, 200, [], [
    'enable_max_depth' => true
]);
# đŸ—ïž Partie 4

Créer une API REST ComplÚte

CRUD complet

Structure d'une API REST 📁

Organisation recommandée

src/
├── Controller/
│   └── Api/
│       ├── ArticleController.php
│       ├── UserController.php
│       └── ProductController.php
├── Entity/
│   ├── Article.php
│   ├── User.php
│   └── Product.php
└── Repository/
    ├── ArticleRepository.php
    ├── UserRepository.php
    └── ProductRepository.php

Convention des routes API

// Toutes les routes commencent par /api
GET    /api/articles         → Liste
GET    /api/articles/5       → DĂ©tail
POST   /api/articles         → CrĂ©er
PUT    /api/articles/5       → Modifier (complet)
PATCH  /api/articles/5       → Modifier (partiel)
DELETE /api/articles/5       → Supprimer

GET - Lister les ressources 📋

src/Controller/Api/ArticleController.php

<?php
namespace App\Controller\Api;

use App\Repository\ArticleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

#[Route('/api', name: 'api_')]
class ArticleController extends AbstractController
{
    #[Route('/articles', name: 'articles_list', methods: ['GET'])]
    public function list(ArticleRepository $repository): JsonResponse
    {
        $articles = $repository->findAll();
        
        return $this->json($articles, 200, [], [
            'groups' => ['article:read']
        ]);
    }
}

Réponse JSON

HTTP/1.1 200 OK
Content-Type: application/json

[
  {"id": 1, "title": "Article 1", "content": "..."},
  {"id": 2, "title": "Article 2", "content": "..."}
]

GET - Afficher une ressource 📄

Méthode show()

#[Route('/articles/{id}', name: 'articles_show', methods: ['GET'])]
public function show(Article $article): JsonResponse
{
    return $this->json($article, 200, [], [
        'groups' => ['article:read']
    ]);
}

Gestion 404 automatique

Symfony génÚre automatiquement une 404 si l'article n'existe pas grùce au ParamConverter !

Réponse JSON

HTTP/1.1 200 OK

{
  "id": 5,
  "title": "Mon article",
  "content": "Contenu...",
  "createdAt": "2025-01-31T10:00:00+00:00"
}

POST - CrĂ©er une ressource ✹

Méthode create()

use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;

#[Route('/articles', name: 'articles_create', methods: ['POST'])]
public function create(
    Request $request,
    SerializerInterface $serializer,
    EntityManagerInterface $em
): JsonResponse {
    // DĂ©sĂ©rialiser JSON → Objet Article
    $article = $serializer->deserialize(
        $request->getContent(),
        Article::class,
        'json'
    );
    
    // Sauvegarder
    $em->persist($article);
    $em->flush();
    
    return $this->json($article, 201, [], [
        'groups' => ['article:read']
    ]);
}

RequĂȘte

POST /api/articles
Content-Type: application/json

{
  "title": "Nouvel article",
  "content": "Contenu de l'article..."
}

Réponse

HTTP/1.1 201 Created

{
  "id": 6,
  "title": "Nouvel article",
  "content": "Contenu..."
}

PUT - Modifier une ressource 🔄

Méthode update()

#[Route('/articles/{id}', name: 'articles_update', methods: ['PUT'])]
public function update(
    Article $article,
    Request $request,
    SerializerInterface $serializer,
    EntityManagerInterface $em
): JsonResponse {
    // Désérialiser dans l'article existant
    $serializer->deserialize(
        $request->getContent(),
        Article::class,
        'json',
        ['object_to_populate' => $article]
    );
    
    $em->flush();
    
    return $this->json($article, 200, [], [
        'groups' => ['article:read']
    ]);
}

RequĂȘte

PUT /api/articles/5

{
  "title": "Titre modifié",
  "content": "Contenu modifié..."
}

DELETE - Supprimer une ressource đŸ—‘ïž

Méthode delete()

#[Route('/articles/{id}', name: 'articles_delete', methods: ['DELETE'])]
public function delete(
    Article $article,
    EntityManagerInterface $em
): JsonResponse {
    $em->remove($article);
    $em->flush();
    
    return $this->json(null, 204);
}

RequĂȘte

DELETE /api/articles/5

Réponse

HTTP/1.1 204 No Content
# 🔐 Partie 5

Authentification JWT

JSON Web Token

C'est quoi JWT ? đŸŽ«

JWT = Token sécurisé contenant les informations de l'utilisateur, signé par le serveur.

Analogie : Le badge d'Ă©vĂ©nement đŸŽŸïž

JWT = badge d'accĂšs Ă  un festival :

Structure d'un JWT đŸ—ïž

Format

eyJhbGc...Header.eyJ1c2Vy...Payload.SflKxwRJ...Signature

Décodé

// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload
{
  "username": "marie@example.com",
  "roles": ["ROLE_USER"],
  "exp": 1640995200
}

// Signature
HMACSHA256(
  base64(header) + "." + base64(payload),
  secret
)

Installer LexikJWTAuthenticationBundle 📩

1. Installation

composer require lexik/jwt-authentication-bundle

2. Générer les clés

php bin/console lexik:jwt:generate-keypair

3. Configuration security.yaml

# config/packages/security.yaml
security:
    firewalls:
        login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success

        api:
            pattern: ^/api
            stateless: true
            jwt: ~
            
    access_control:
        - { path: ^/api/login, roles: PUBLIC_ACCESS }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

Utiliser l'authentification JWT 🔐

1. Se connecter (obtenir le token)

POST /api/login_check
Content-Type: application/json

{
  "username": "marie@example.com",
  "password": "motdepasse123"
}

// Réponse
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

2. Utiliser le token

GET /api/articles
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// Si token valide → 200 OK
// Si token invalide → 401 Unauthorized

3. Récupérer l'utilisateur connecté

#[Route('/api/profile', methods: ['GET'])]
public function profile(): JsonResponse
{
    $user = $this->getUser();  // Utilisateur du token
    
    return $this->json([
        'email' => $user->getEmail(),
        'roles' => $user->getRoles()
    ]);
}
# ✅ Partie 6

Validation des Données

Validator component

Valider les donnĂ©es reçues ✅

Contraintes dans l'entité

use Symfony\Component\Validator\Constraints as Assert;

class Article
{
    #[Assert\NotBlank(message: 'Le titre est obligatoire')]
    #[Assert\Length(
        min: 3,
        max: 255,
        minMessage: 'Le titre doit faire au moins 3 caractĂšres'
    )]
    private ?string $title = null;
    
    #[Assert\NotBlank]
    #[Assert\Length(min: 10)]
    private ?string $content = null;
    
    #[Assert\Email]
    private ?string $authorEmail = null;
}

Valider dans le contrîleur 🔍

Méthode create() avec validation

use Symfony\Component\Validator\Validator\ValidatorInterface;

#[Route('/articles', methods: ['POST'])]
public function create(
    Request $request,
    SerializerInterface $serializer,
    ValidatorInterface $validator,
    EntityManagerInterface $em
): JsonResponse {
    $article = $serializer->deserialize(
        $request->getContent(),
        Article::class,
        'json'
    );
    
    // Valider
    $errors = $validator->validate($article);
    
    if (count($errors) > 0) {
        $errorsArray = [];
        foreach ($errors as $error) {
            $errorsArray[$error->getPropertyPath()] = $error->getMessage();
        }
        
        return $this->json(['errors' => $errorsArray], 400);
    }
    
    $em->persist($article);
    $em->flush();
    
    return $this->json($article, 201);
}

Format de rĂ©ponse d'erreur 📛

RequĂȘte invalide

POST /api/articles

{
  "title": "AB",
  "content": "Court"
}

Réponse 400

HTTP/1.1 400 Bad Request

{
  "errors": {
    "title": "Le titre doit faire au moins 3 caractĂšres",
    "content": "This value is too short. It should have 10 characters or more."
  }
}
# 📄 Partie 7

Pagination

Gérer les grandes listes

ImplĂ©menter la pagination 📄

ContrĂŽleur avec pagination

#[Route('/api/articles', methods: ['GET'])]
public function list(
    Request $request,
    ArticleRepository $repository
): JsonResponse {
    $page = $request->query->getInt('page', 1);
    $limit = $request->query->getInt('limit', 20);
    
    $offset = ($page - 1) * $limit;
    
    $articles = $repository->findBy(
        [],
        ['createdAt' => 'DESC'],
        $limit,
        $offset
    );
    
    $total = $repository->count([]);
    
    return $this->json([
        'data' => $articles,
        'meta' => [
            'current_page' => $page,
            'items_per_page' => $limit,
            'total_items' => $total,
            'total_pages' => ceil($total / $limit)
        ]
    ], 200, [], ['groups' => ['article:read']]);
}

RequĂȘte

GET /api/articles?page=2&limit=10
# 🔍 Partie 8

Filtres et Recherche

Query parameters

Ajouter des filtres 🔍

Exemples d'URLs

GET /api/articles?status=published
GET /api/articles?author=5
GET /api/articles?search=symfony
GET /api/articles?category=tech&status=published

Implémentation

#[Route('/api/articles', methods: ['GET'])]
public function list(Request $request, ArticleRepository $repo): JsonResponse
{
    $filters = [
        'status' => $request->query->get('status'),
        'author' => $request->query->get('author'),
    ];
    
    $filters = array_filter($filters);
    
    $articles = $repo->findBy($filters);
    
    return $this->json($articles, 200, [], ['groups' => ['article:read']]);
}
# ⚡ Partie 9

API Platform

Framework API complet

Qu'est-ce qu'API Platform ? ⚡

API Platform = Framework qui génÚre automatiquement une API REST complÚte.

✅ Avantages :

Installer API Platform 📩

Installation

composer require api

Activer sur une entité

use ApiPlatform\Metadata\ApiResource;

#[ApiResource]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    private ?int $id = null;
    
    #[ORM\Column(length: 255)]
    private ?string $title = null;
}

✅ C'est tout !

API Platform génÚre automatiquement :

Documentation automatique 📚

API Platform génÚre automatiquement une documentation interactive !

Accéder à la documentation

http://localhost:8000/api

// Interface Swagger interactive :
// - Liste de tous les endpoints
// - Tester les requĂȘtes directement
// - Voir les schémas JSON

✅ Avantages :

# đŸ§Ș Partie 10

Tester une API

Outils de test

Outils pour tester une API đŸ§Ș

1. Postman

2. Insomnia

3. cURL (ligne de commande)

# GET
curl http://localhost:8000/api/articles

# POST
curl -X POST http://localhost:8000/api/articles \
  -H "Content-Type: application/json" \
  -d '{"title":"Nouvel article","content":"..."}'

# Avec JWT
curl http://localhost:8000/api/articles \
  -H "Authorization: Bearer TOKEN"
# 🎓 RĂ©capitulatif

Ce qu'on a appris

Concepts clĂ©s - RĂ©sumĂ© 📚

Concept Description
API REST Interface pour communiquer entre applications via HTTP
JSON Format d'échange de données standard
SĂ©rialisation Objet PHP → JSON
CRUD Create, Read, Update, Delete
JWT Token d'authentification sécurisé
Validation Vérifier les données avant sauvegarde
Pagination Découper les grandes listes
API Platform Générateur d'API automatique

Bonnes pratiques API REST 💡

  1. Utiliser les bons codes HTTP
    200 OK, 201 Created, 400 Bad Request, 404 Not Found...
  2. Versionner l'API
    /api/v1/articles, /api/v2/articles
  3. Toujours valider les données
    Utiliser le Validator component
  4. Paginer les listes
    Éviter de retourner 10 000 rĂ©sultats
  5. Documenter l'API
    API Platform ou Swagger
  6. Sécuriser avec JWT
    Authentification stateless
  7. Groupes de sérialisation
    ContrÎler quels champs sont exposés

Ressources pour aller plus loin 📚

Symfony Serializer

🌐 https://symfony.com/doc/current/components/serializer.html

LexikJWTAuthenticationBundle

🌐 https://github.com/lexik/LexikJWTAuthenticationBundle

API Platform

🌐 https://api-platform.com

Ce que vous maĂźtrisez maintenant ! đŸ’Ș

Compétences acquises :

🎓 FĂ©licitations !

Vous maĂźtrisez les API REST avec Symfony

Vous savez maintenant :

✅ CrĂ©er des APIs REST professionnelles

✅ SĂ©curiser avec JWT

✅ Utiliser API Platform

✅ Documenter automatiquement

💡 Conseil final :

Les APIs REST sont essentielles dans le développement moderne. Avec Symfony et API Platform, vous pouvez créer des APIs professionnelles rapidement et efficacement !

Formation Symfony complùte ! 🚀

Formation DWWM - 2026