When not to use API Platform?
API Platform is a powerful tool, but for simple projects it can be overkill. Got a simple CRUD with 3 entities? Do it by hand — less magic, more control.
Controller structure
<?php
namespace App\Api\Controller;
use App\Entity\BlogArticle;
use App\Repository\BlogArticleRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route('/api/v1', name: 'api_v1_', format: 'json')]
class ArticleApiController extends AbstractController
{
public function __construct(
private readonly BlogArticleRepository $repo,
private readonly EntityManagerInterface $em,
private readonly ValidatorInterface $validator,
) {}
#[Route('/articles', name: 'articles_index', methods: ['GET'])]
public function index(): JsonResponse
{
return $this->json(
$this->repo->findPublished(),
200,
[],
['groups' => 'article:list']
);
}
#[Route('/articles/{id}', name: 'articles_show', methods: ['GET'])]
public function show(BlogArticle $article): JsonResponse
{
return $this->json($article, 200, [], ['groups' => 'article:show']);
}
}
Serialization groups
Thanks to groups you control which fields are returned — different for the list, different for the details:
use Symfony\Component\Serializer\Annotation\Groups;
class BlogArticle
{
#[Groups(['article:list', 'article:show'])]
private ?int $id = null;
#[Groups(['article:list', 'article:show'])]
private ?string $title = null;
#[Groups(['article:show'])] // only in the details
private ?string $content = null;
#[Groups(['article:list', 'article:show'])]
private ?\DateTimeImmutable $publishedAt = null;
}
Input validation
use Symfony\Component\Validator\Constraints as Assert;
class BlogArticle
{
#[Assert\NotBlank(message: 'Title cannot be empty.')]
#[Assert\Length(min: 5, max: 255)]
private ?string $title = null;
#[Assert\NotBlank]
#[Assert\Length(min: 50)]
private ?string $content = null;
}
Running validation in the controller:
$violations = $this->validator->validate($article);
if (count($violations) > 0) {
$errors = [];
foreach ($violations as $v) {
$errors[] = [
'field' => $v->getPropertyPath(),
'message' => $v->getMessage(),
];
}
return $this->json(['violations' => $errors], 422);
}
Consistent error format
Always return the same format — it makes debugging on the frontend easier:
{
"status": "error",
"code": 422,
"message": "Validation failed.",
"violations": [
{ "field": "title", "message": "Title cannot be empty." },
{ "field": "content", "message": "This value should not be blank." }
]
}
HTTP codes — cheat sheet
| Code | Meaning | When |
|---|---|---|
| 200 | OK | GET success |
| 201 | Created | POST success |
| 204 | No Content | DELETE success |
| 400 | Bad Request | malformed JSON |
| 401 | Unauthorized | missing token |
| 403 | Forbidden | no permissions |
| 404 | Not Found | resource doesn't exist |
| 422 | Unprocessable | validation errors |
| 500 | Server Error | application error |