Programming

REST API in Symfony without API Platform — serializer and validation

Jan Kowalski 06.10.2026 208 wyświetleń 0 komentarzy
This translation was generated by AI and may contain inaccuracies. Read the original (PL)

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
Tagi: #PHP #Symfony #REST API #Security
Udostępnij: Facebook X LinkedIn

Komentarze (0)


Bądź pierwszy — zostaw komentarz!

Zaloguj się, aby dodać komentarz.