API Platform with Symfony in 2026: Architecture, State Providers, and Interview Questions

Master API Platform 4.2 with Symfony: State Providers, Processors, Object Mapper, JSON Streamer performance optimizations, and common interview questions for senior developers.

API Platform Symfony architecture diagram with REST API workflow

API Platform 4.2 transforms how Symfony applications expose REST and GraphQL APIs. This version introduces the Symfony Object Mapper for clean resource separation, the JSON Streamer for significant performance gains, and a redesigned filter system. For developers preparing technical interviews, understanding these architectural patterns distinguishes senior candidates from juniors.

API Platform 4.2 Requirements

API Platform 4.2 requires Symfony 7.4 or 8.0. Support for Symfony 6.4 and 7.0-7.3 has been dropped. The JSON Streamer delivers up to 32% more requests per second on collection endpoints.

Setting Up API Platform 4.2 with Symfony

API Platform installs through Symfony Flex with automatic configuration. The default setup handles most use cases while remaining fully customizable for complex domain requirements.

bash
# Install API Platform
composer require api-platform/symfony

# The API documentation is available at /api/
# Open http://localhost:8000/api/ after starting the server
symfony serve

The Flex recipe configures serialization groups, Doctrine integration, and OpenAPI documentation generation. API resources expose CRUD operations by adding a single attribute to entity classes.

src/Entity/Book.phpphp
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(security: "is_granted('ROLE_ADMIN')"),
        new Put(security: "is_granted('ROLE_ADMIN')"),
        new Delete(security: "is_granted('ROLE_ADMIN')")
    ],
    normalizationContext: ['groups' => ['book:read']],
    denormalizationContext: ['groups' => ['book:write']]
)]
class Book
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Groups(['book:read', 'book:write'])]
    private string $title;

    #[ORM\Column(type: 'text')]
    #[Groups(['book:read', 'book:write'])]
    private string $description;

    #[ORM\Column]
    #[Groups(['book:read'])]
    private \DateTimeImmutable $createdAt;

    // Getters and setters...
}

This configuration generates five endpoints with automatic validation, serialization, and OpenAPI documentation.

State Providers: Fetching Data from Any Source

State Providers control how API Platform retrieves data for GET operations. The default Doctrine provider handles entity fetching, but custom providers enable integration with external APIs, Elasticsearch, or cached data.

src/State/BookStateProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\BookRepository;
use Psr\Cache\CacheItemPoolInterface;

final class BookStateProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private CacheItemPoolInterface $cache
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        // Single item retrieval
        if (isset($uriVariables['id'])) {
            $cacheKey = sprintf('book_%d', $uriVariables['id']);
            $item = $this->cache->getItem($cacheKey);
            
            if ($item->isHit()) {
                return $item->get();
            }
            
            $book = $this->repository->find($uriVariables['id']);
            $item->set($book)->expiresAfter(3600);
            $this->cache->save($item);
            
            return $book;
        }

        // Collection retrieval with custom filtering
        return $this->repository->findActiveBooks();
    }
}

Register the provider on specific operations:

php
#[ApiResource(
    operations: [
        new GetCollection(provider: BookStateProvider::class),
        new Get(provider: BookStateProvider::class),
        // Other operations use default Doctrine provider
        new Post(),
        new Put(),
    ]
)]
class Book { /* ... */ }

State Processors: Handling Mutations with Business Logic

State Processors handle POST, PUT, PATCH, and DELETE operations. They receive deserialized data and apply business logic before persistence.

src/State/BookStateProcessor.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

final class BookStateProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $em,
        private MailerInterface $mailer,
        private ProcessorInterface $persistProcessor // Decorated Doctrine processor
    ) {}

    public function process(
        mixed $data,
        Operation $operation,
        array $uriVariables = [],
        array $context = []
    ): mixed {
        // Pre-persist business logic
        if ($data instanceof Book && $operation instanceof Post) {
            $data->setCreatedAt(new \DateTimeImmutable());
            $data->setSlug($this->generateSlug($data->getTitle()));
        }

        // Delegate to Doctrine processor
        $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);

        // Post-persist notification
        if ($operation instanceof Post) {
            $this->notifyNewBook($data);
        }

        return $result;
    }

    private function generateSlug(string $title): string
    {
        return strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', $title));
    }

    private function notifyNewBook(Book $book): void
    {
        $email = (new Email())
            ->to('catalog@example.com')
            ->subject('New book added: ' . $book->getTitle())
            ->text('A new book has been added to the catalog.');
        $this->mailer->send($email);
    }
}
Processor Decoration

Decorating the default Doctrine processor with #[AsDecorator] preserves persistence behavior while adding custom logic. This pattern avoids duplicating ORM operations.

Object Mapper: Separating API Resources from Entities

API Platform 4.2 integrates the Symfony Object Mapper component to decouple API representations from domain entities. This separation enables different read/write models and protects internal entity structures.

src/ApiResource/BookResource.phpphp
namespace App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Entity\Book;
use Symfony\Component\ObjectMapper\Attribute\Map;

#[ApiResource(
    shortName: 'Book',
    operations: [
        new GetCollection(),
        new Get()
    ]
)]
#[Map(target: Book::class)]
class BookResource
{
    public ?int $id = null;
    
    public string $title;
    
    public string $description;
    
    // Computed field not in entity
    public int $wordCount;
    
    // Formatted date for API consumers
    public string $publishedDate;
}

The mapper provider transforms entities to resources automatically:

src/State/BookResourceProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\BookResource;
use App\Repository\BookRepository;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

final class BookResourceProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private ObjectMapperInterface $mapper
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if (isset($uriVariables['id'])) {
            $book = $this->repository->find($uriVariables['id']);
            return $book ? $this->toResource($book) : null;
        }

        return array_map(
            fn(Book $book) => $this->toResource($book),
            $this->repository->findAll()
        );
    }

    private function toResource(Book $book): BookResource
    {
        $resource = $this->mapper->map($book, BookResource::class);
        $resource->wordCount = str_word_count($book->getDescription());
        $resource->publishedDate = $book->getCreatedAt()->format('F j, Y');
        return $resource;
    }
}

Ready to ace your Symfony interviews?

Practice with our interactive simulators, flashcards, and technical tests.

JSON Streamer: 32% Performance Improvement

The JSON Streamer component serializes large collections without loading entire datasets into memory. Benchmarks on the Sylius API showed a 32.4% increase in requests per second.

Enable streaming on resource or operation level:

php
#[ApiResource(
    operations: [
        new GetCollection(
            jsonStream: true,  // Enable JSON streaming
            paginationItemsPerPage: 100
        ),
        new Get()
    ]
)]
class Book { /* ... */ }

Streaming particularly benefits:

  • Collection endpoints with 50+ items
  • Resources with nested relationships
  • APIs serving mobile clients with limited bandwidth

The OpenAPI specification has also been optimized. JSON Schema mutualization reduces file size by 30%, improving documentation load times.

Interview Questions: API Platform Architecture

Technical interviews for Symfony positions frequently cover API Platform patterns. These questions assess understanding of the framework's architecture beyond basic CRUD operations.

"Explain the difference between State Providers and Processors"

Expected answer: State Providers fetch data for read operations (GET). They return entities, DTOs, or arrays. State Processors handle write operations (POST, PUT, PATCH, DELETE). They receive deserialized input and execute business logic before persistence. The separation follows CQRS principles: queries through Providers, commands through Processors.

"When would you use a custom API Resource instead of exposing an entity directly?"

Expected answer: Custom resources apply when:

  • The API representation differs from the database schema
  • Computed fields require aggregation from multiple entities
  • Write and read models need different structures
  • Internal entity fields must remain hidden from API consumers
  • Version compatibility requires stable contracts while entities evolve

"How does API Platform handle validation?"

Expected answer: API Platform uses Symfony Validator constraints on entity properties. Validation runs automatically during deserialization before the State Processor executes. Validation groups control which constraints apply per operation. Custom validators integrate through standard Symfony mechanisms.

php
#[ApiResource(
    operations: [
        new Post(validationContext: ['groups' => ['create']]),
        new Put(validationContext: ['groups' => ['update']])
    ]
)]
class Book
{
    #[Assert\NotBlank(groups: ['create', 'update'])]
    private string $title;

    #[Assert\Isbn(groups: ['create'])]
    private string $isbn;  // Required only on creation
}
Common Interview Mistake

Candidates often describe validation as "automatic" without mentioning validation groups or custom constraints. Interviewers look for understanding of how to customize validation per operation.

"What security mechanisms does API Platform provide?"

Expected answer: API Platform integrates with Symfony Security through:

  • security attribute on operations for role-based access
  • securityPostDenormalize for object-level checks after data binding
  • Voters for complex authorization logic
  • Rate limiting through Symfony Rate Limiter integration
php
#[ApiResource(
    operations: [
        new Get(
            security: "is_granted('ROLE_USER')"
        ),
        new Put(
            security: "is_granted('ROLE_ADMIN') or object.getOwner() == user",
            securityPostDenormalize: "is_granted('BOOK_EDIT', object)"
        )
    ]
)]

Filters and Pagination: Advanced Query Patterns

API Platform filters enable clients to query collections with URL parameters. The filter system in 4.2 has been redesigned for better extensibility.

php
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: [
    'title' => 'partial',      // LIKE %value%
    'author.name' => 'exact'   // Nested property
])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
#[ApiFilter(OrderFilter::class, properties: ['title', 'createdAt'])]
class Book { /* ... */ }

Generated endpoints:

text
GET /api/books?title=symfony           # Search by title
GET /api/books?createdAt[after]=2026-01-01  # Date range
GET /api/books?order[createdAt]=desc   # Sorting

Testing API Platform Resources

API Platform provides a testing client that simplifies functional tests. The ApiTestCase class offers assertions specific to API responses.

tests/Api/BookTest.phpphp
namespace App\Tests\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;

class BookTest extends ApiTestCase
{
    public function testGetCollection(): void
    {
        $response = static::createClient()->request('GET', '/api/books');

        $this->assertResponseIsSuccessful();
        $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
        $this->assertJsonContains([
            '@context' => '/api/contexts/Book',
            '@type' => 'Collection'
        ]);
    }

    public function testCreateBook(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => [
                'title' => 'Symfony Best Practices',
                'description' => 'A guide to modern Symfony development'
            ],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(201);
        $this->assertJsonContains(['title' => 'Symfony Best Practices']);
    }

    public function testCreateBookValidationFails(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => ['description' => 'Missing title'],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(422);
        $this->assertJsonContains([
            'violations' => [
                ['propertyPath' => 'title', 'message' => 'This value should not be blank.']
            ]
        ]);
    }
}

Key Takeaways for API Platform with Symfony

  • State Providers handle GET operations, State Processors handle mutations. This separation enables clean architecture with custom data sources and business logic
  • The Object Mapper component decouples API resources from Doctrine entities, allowing different read/write models and protecting internal structures
  • JSON Streamer delivers 32% performance improvement on collection endpoints by serializing without full memory allocation
  • Security integrates through Symfony's standard mechanisms: security expressions, Voters, and the Rate Limiter component
  • Validation groups customize constraint enforcement per operation
  • Filters expose query parameters automatically, with search, date, and order filters covering most use cases
  • Interview questions focus on architectural decisions: when to use custom providers, how to separate read/write models, and security implementation patterns

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Daily challenge

Can you spot the bug in Symfony?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 8, 2026

Tags

#api-platform
#symfony
#rest-api
#state-providers
#interview

Share

Related articles