Créer des APIs modernes et performantes
REST âą JSON âą HTTP âą Serialization âą JWT âą API Platform
Formation DWWM - 2026
Concepts fondamentaux
API (Application Programming Interface) = Interface qui permet Ă des applications de communiquer entre elles.
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 = Style d'architecture pour les APIs web, basé sur HTTP.
| 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...) |
| 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 :
GET, POST, PUT, PATCH, DELETE
| 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.
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 !
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 !
| 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 |
| 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 |
| Code | Signification | Usage |
|---|---|---|
| 500 Internal Error | Erreur serveur | Exception non gérée |
| 503 Unavailable | Service indisponible | Maintenance |
Serializer Component
JSON = Format d'échange de données standard pour les APIs REST.
{
"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 = emballer un objet pour l'envoyer :
On ne peut pas envoyer directement un objet PHP sur HTTP. Il faut le transformer en JSON !
composer require symfony/serializer
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 !
ContrÎler quels champs sont sérialisés avec les groupes.
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;
}
return $this->json($article, 200, [], [
'groups' => ['article:read']
]);
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...
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
]);
CRUD complet
src/
âââ Controller/
â âââ Api/
â âââ ArticleController.php
â âââ UserController.php
â âââ ProductController.php
âââ Entity/
â âââ Article.php
â âââ User.php
â âââ Product.php
âââ Repository/
âââ ArticleRepository.php
âââ UserRepository.php
âââ ProductRepository.php
// 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
<?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']
]);
}
}
HTTP/1.1 200 OK
Content-Type: application/json
[
{"id": 1, "title": "Article 1", "content": "..."},
{"id": 2, "title": "Article 2", "content": "..."}
]
#[Route('/articles/{id}', name: 'articles_show', methods: ['GET'])]
public function show(Article $article): JsonResponse
{
return $this->json($article, 200, [], [
'groups' => ['article:read']
]);
}
Symfony génÚre automatiquement une 404 si l'article n'existe pas grùce au ParamConverter !
HTTP/1.1 200 OK
{
"id": 5,
"title": "Mon article",
"content": "Contenu...",
"createdAt": "2025-01-31T10:00:00+00:00"
}
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']
]);
}
POST /api/articles
Content-Type: application/json
{
"title": "Nouvel article",
"content": "Contenu de l'article..."
}
HTTP/1.1 201 Created
{
"id": 6,
"title": "Nouvel article",
"content": "Contenu..."
}
#[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']
]);
}
PUT /api/articles/5
{
"title": "Titre modifié",
"content": "Contenu modifié..."
}
#[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);
}
DELETE /api/articles/5
HTTP/1.1 204 No Content
JSON Web Token
JWT = Token sécurisé contenant les informations de l'utilisateur, signé par le serveur.
JWT = badge d'accĂšs Ă un festival :
eyJhbGc...Header.eyJ1c2Vy...Payload.SflKxwRJ...Signature
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"username": "marie@example.com",
"roles": ["ROLE_USER"],
"exp": 1640995200
}
// Signature
HMACSHA256(
base64(header) + "." + base64(payload),
secret
)
composer require lexik/jwt-authentication-bundle
php bin/console lexik:jwt:generate-keypair
# 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 }
POST /api/login_check
Content-Type: application/json
{
"username": "marie@example.com",
"password": "motdepasse123"
}
// Réponse
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
GET /api/articles
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// Si token valide â 200 OK
// Si token invalide â 401 Unauthorized
#[Route('/api/profile', methods: ['GET'])]
public function profile(): JsonResponse
{
$user = $this->getUser(); // Utilisateur du token
return $this->json([
'email' => $user->getEmail(),
'roles' => $user->getRoles()
]);
}
Validator component
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;
}
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);
}
POST /api/articles
{
"title": "AB",
"content": "Court"
}
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."
}
}
Gérer les grandes listes
#[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']]);
}
GET /api/articles?page=2&limit=10
Query parameters
GET /api/articles?status=published
GET /api/articles?author=5
GET /api/articles?search=symfony
GET /api/articles?category=tech&status=published
#[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']]);
}
Framework API complet
API Platform = Framework qui génÚre automatiquement une API REST complÚte.
composer require api
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 :
API Platform génÚre automatiquement une documentation interactive !
http://localhost:8000/api
// Interface Swagger interactive :
// - Liste de tous les endpoints
// - Tester les requĂȘtes directement
// - Voir les schémas JSON
â Avantages :
Outils de test
# 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"
| 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 |
đ https://symfony.com/doc/current/components/serializer.html
đ https://github.com/lexik/LexikJWTAuthenticationBundle
đ https://api-platform.com
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