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 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 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.
# 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 serveThe Flex recipe configures serialization groups, Doctrine integration, and OpenAPI documentation generation. API resources expose CRUD operations by adding a single attribute to entity classes.
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.
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:
#[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.
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);
}
}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.
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:
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:
#[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.
#[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
}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:
securityattribute on operations for role-based accesssecurityPostDenormalizefor object-level checks after data binding- Voters for complex authorization logic
- Rate limiting through Symfony Rate Limiter integration
#[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.
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 # SortingTesting API Platform Resources
API Platform provides a testing client that simplifies functional tests. The ApiTestCase class offers assertions specific to API responses.
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:
securityexpressions, 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.
Can you spot the bug in Symfony?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 8, 2026
Tags
Share
Related articles

API Platform Symfony REST: Complete Tutorial and Interview Questions 2026
Build production-ready REST APIs with API Platform 4 and Symfony 7. Learn State Providers, Processors, filters, and master the most common interview questions about API Platform.

API Platform GraphQL Symfony: Schemas, Mutations and Interview Questions 2026
Master API Platform GraphQL integration with Symfony. Learn schema generation, mutations, subscriptions, custom resolvers and prepare for technical interviews with real-world examples.

Symfony REST API Security: JWT Authentication and Interview Questions 2026
Master Symfony REST API security with JWT authentication using LexikJWTAuthenticationBundle. Covers token generation, refresh tokens, security voters, and common interview questions.