# 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. - Published: 2026-09-08 - Updated: 2026-09-08 - Author: Anthony Fillion-Maillet - Tags: api-platform, symfony, rest-api, state-providers, interview - Reading time: 9 min --- 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. ```php // src/Entity/Book.php 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. ```php // src/State/BookStateProvider.php 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](https://api-platform.com/docs/core/state-processors/) handle POST, PUT, PATCH, and DELETE operations. They receive deserialized data and apply business logic before persistence. ```php // src/State/BookStateProcessor.php 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](https://symfony.com/doc/current/components/object_mapper.html) to decouple API representations from domain entities. This separation enables different read/write models and protects internal entity structures. ```php // src/ApiResource/BookResource.php 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: ```php // src/State/BookResourceProvider.php 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; } } ``` ## 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](https://soyuka.me/api-platform-4-2-redefining-api-development/). 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](https://api-platform.com/docs/core/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: ``` 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. ```php // tests/Api/BookTest.php 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](/technologies/symfony/interview-questions/testing) 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/symfony/api-platform-symfony-architecture-interview-questions