# API Platform Symfony REST: Complete Tutorial en Sollicitatievragen 2026 > REST APIs bouwen met API Platform 4.3 en Symfony 7. State Providers, Processors, serialisatiegroepen en veelvoorkomende sollicitatievragen voor Symfony-ontwikkelaars. - Published: 2026-08-25 - Updated: 2026-08-25 - Author: Anthony Fillion-Maillet - Tags: api-platform, symfony, rest-api, php, tutorial - Reading time: 12 min --- API Platform 4.3 transformeert Symfony in een krachtig REST API-framework met automatische OpenAPI-documentatie, content negotiation en een schone architectuur die lees- en schrijfoperaties scheidt. Deze tutorial behandelt de installatie, geavanceerde patterns en de sollicitatievragen die senior ontwikkelaars van beginners onderscheiden. > **API Platform 4 Architectuur** > > API Platform 4 gebruikt State Providers voor GET-operaties en State Processors voor POST/PUT/PATCH/DELETE. Deze scheiding sluit aan bij CQRS-principes en vereenvoudigt het testen aanzienlijk. ## API Platform Installeren op Symfony 7 De installatie van API Platform vereist Symfony 7.2 of hoger. De bundle integreert standaard met Doctrine ORM, maar ondersteunt ook aangepaste databronnen via het Provider/Processor-pattern. ```bash # API Platform installeren met Symfony Flex composer require api # Installatie verifiëren php bin/console debug:router | grep api ``` De `api`-recipe installeert `api-platform/symfony` samen met de serializer-, validator- en property-access-componenten. Symfony Flex configureert de routes automatisch onder `/api`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'My API' version: '1.0.0' formats: jsonld: ['application/ld+json'] json: ['application/json'] docs_formats: jsonld: ['application/ld+json'] jsonopenapi: ['application/vnd.openapi+json'] html: ['text/html'] defaults: stateless: true cache_headers: vary: ['Content-Type', 'Authorization', 'Accept-Language'] ``` De instelling `stateless: true` schakelt PHP-sessies uit voor alle API-endpoints, vermindert geheugenoverhead en maakt horizontale schaalbaarheid mogelijk. ## De Eerste API Resource Maken met Attributen API Platform 4 gebruikt PHP 8-attributen om API resources te declareren. Elke entity wordt een API-endpoint via het `#[ApiResource]`-attribuut. ```php // src/Entity/Product.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')") ], paginationItemsPerPage: 30 )] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 3, max: 255)] private string $name; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] #[Assert\Positive] private string $price; #[ORM\Column] private \DateTimeImmutable $createdAt; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } public function getId(): ?int { return $this->id; } public function getName(): string { return $this->name; } public function setName(string $name): static { $this->name = $name; return $this; } public function getPrice(): string { return $this->price; } public function setPrice(string $price): static { $this->price = $price; return $this; } public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } } ``` De `security`-parameter op elke operatie maakt gebruik van Symfony's Expression Language. De `is_granted()`-functie controleert voter-beslissingen, waardoor rolgebaseerde toegangscontrole mogelijk is zonder aangepaste controllers. ## Serialisatiegroepen voor Response-vormgeving Serialisatiegroepen bepalen welke eigenschappen in API-responses verschijnen. Verschillende operaties kunnen verschillende velden van dezelfde entity tonen. ```php // src/Entity/User.php namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity] #[ApiResource( operations: [ new GetCollection(normalizationContext: ['groups' => ['user:list']]), new Get(normalizationContext: ['groups' => ['user:read']]), new Post( normalizationContext: ['groups' => ['user:read']], denormalizationContext: ['groups' => ['user:write']] ) ] )] class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['user:list', 'user:read'])] private ?int $id = null; #[ORM\Column(length: 180)] #[Groups(['user:list', 'user:read', 'user:write'])] private string $email; #[ORM\Column] #[Groups(['user:write'])] private string $password; #[ORM\Column] #[Groups(['user:read'])] private \DateTimeImmutable $registeredAt; #[ORM\Column(type: 'json')] #[Groups(['user:read'])] private array $roles = []; } ``` De `normalizationContext` regelt de output (PHP naar JSON). De `denormalizationContext` regelt de input (JSON naar PHP). Het `password`-veld gebruikt alleen `user:write`, waardoor het nooit in responses verschijnt. ## State Providers voor Aangepaste Databronnen State Providers halen gegevens op voor GET-operaties. De standaard `ItemProvider` en `CollectionProvider` gebruiken Doctrine, maar aangepaste providers ondersteunen elke databron: Elasticsearch, externe API's of berekende waarden. ```php // src/State/ProductStatsProvider.php namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\Repository\ProductRepository; use App\Dto\ProductStats; final readonly class ProductStatsProvider implements ProviderInterface { public function __construct( private ProductRepository $productRepository ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): ProductStats { $totalProducts = $this->productRepository->count([]); $averagePrice = $this->productRepository->getAveragePrice(); $lowStockCount = $this->productRepository->countLowStock(threshold: 10); return new ProductStats( totalProducts: $totalProducts, averagePrice: $averagePrice, lowStockCount: $lowStockCount ); } } ``` ```php // src/Dto/ProductStats.php namespace App\Dto; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use App\State\ProductStatsProvider; #[ApiResource( operations: [ new Get( uriTemplate: '/products/stats', provider: ProductStatsProvider::class ) ] )] final readonly class ProductStats { public function __construct( public int $totalProducts, public float $averagePrice, public int $lowStockCount ) {} } ``` De `ProductStats` DTO (Data Transfer Object) is geen Doctrine entity. Het vertegenwoordigt berekende gegevens beschikbaar op `/api/products/stats`. De provider berekent waarden bij elke aanvraag. ## State Processors voor Schrijfoperaties State Processors verwerken POST-, PUT-, PATCH- en DELETE-operaties. Aangepaste processors maken het mogelijk om bedrijfslogica uit te voeren voor of na de persistentie. ```php // src/State/UserRegistrationProcessor.php namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; final readonly class UserRegistrationProcessor implements ProcessorInterface { public function __construct( private EntityManagerInterface $entityManager, private UserPasswordHasherInterface $passwordHasher ) {} public function process( mixed $data, Operation $operation, array $uriVariables = [], array $context = [] ): User { // $data is de gedeserialiseerde User entity uit de request body $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); $this->entityManager->persist($data); $this->entityManager->flush(); return $data; } } ``` De processor wordt gekoppeld aan de POST-operatie op de User entity: ```php #[ApiResource( operations: [ new Post( processor: UserRegistrationProcessor::class, denormalizationContext: ['groups' => ['user:create']] ) ] )] ``` De processor hasht het wachtwoord voordat Doctrine de entity persisteert. De `plainPassword`-eigenschap gebruikt een write-only serialisatiegroep en wordt nooit in de database opgeslagen. ## Filters voor Query Parameters API Platform biedt ingebouwde filters voor zoeken, sorteren en filteren van collections. Filters voegen query parameters toe aan GET collection endpoints. ```php // src/Entity/Article.php namespace App\Entity; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\DateFilter; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; #[ORM\Entity] #[ApiResource] #[ApiFilter(SearchFilter::class, properties: [ 'title' => 'partial', 'author.name' => 'exact', 'category' => 'exact' ])] #[ApiFilter(DateFilter::class, properties: ['publishedAt'])] #[ApiFilter(OrderFilter::class, properties: ['publishedAt', 'title'])] class Article { // Entity eigenschappen... } ``` De filterconfiguratie activeert deze query parameters: - `GET /api/articles?title=symfony` (gedeeltelijke overeenkomst op titel) - `GET /api/articles?author.name=John` (exacte overeenkomst op gerelateerde entity) - `GET /api/articles?publishedAt[after]=2026-01-01` (datumbereik) - `GET /api/articles?order[publishedAt]=desc` (sortering) Aangepaste filters breiden `AbstractFilter` uit voor complexe query-logica die ingebouwde filters niet kunnen uitdrukken. ## Foutafhandeling en Validatie API Platform integreert met Symfony Validator. Validatiefouten retourneren 422 Unprocessable Entity met RFC 7807-probleemdetails. ```php // src/Entity/Order.php use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] #[ApiResource] class Order { #[ORM\Column] #[Assert\NotBlank(message: 'Bestelhoeveelheid is verplicht')] #[Assert\Positive(message: 'Hoeveelheid moet groter zijn dan nul')] #[Assert\LessThanOrEqual(value: 100, message: 'Maximaal 100 artikelen per bestelling')] private int $quantity; } ``` Een validatiefout retourneert: ```json { "@type": "ConstraintViolationList", "status": 422, "violations": [ { "propertyPath": "quantity", "message": "Hoeveelheid moet groter zijn dan nul" } ] } ``` Aangepaste validators met class-level constraints behandelen cross-field validatie, zoals verzekeren dat de einddatum van een bestelling na de startdatum ligt. ## Sollicitatievragen: Diepgaande API Platform Kennis Technische sollicitaties toetsen begrip voorbij basisgebruik. Deze vragen komen vaak voor bij Symfony backend-posities. **V: Hoe verschilt API Platform van het handmatig schrijven van controllers?** API Platform genereert CRUD-operaties uit entity-metadata. Een enkel `#[ApiResource]`-attribuut produceert GET-, POST-, PUT-, PATCH- en DELETE-endpoints met OpenAPI-documentatie, content negotiation, paginering en validatie. Handmatige controllers vereisen het apart implementeren van elke functie. API Platform vermindert boilerplate-code met 70-80% voor standaard REST-operaties en blijft uitbreidbaar voor aangepaste logica via State Providers en Processors. **V: Leg het verschil uit tussen State Providers en State Processors.** State Providers behandelen het ophalen van gegevens (GET-operaties). Ze implementeren `ProviderInterface::provide()` en retourneren entities, DTO's of collections. State Processors behandelen gegevensmutatie (POST, PUT, PATCH, DELETE). Ze implementeren `ProcessorInterface::process()` en ontvangen het gedeserialiseerde object uit de request body. Deze scheiding volgt CQRS-principes: lees- en schrijfoperaties hebben gescheiden codepaden. **V: Hoe voorkom je het blootstellen van gevoelige velden in API-responses?** Serialisatiegroepen regelen veldzichtbaarheid. Gevoelige velden (wachtwoorden, interne ID's, auditgegevens) worden toegewezen aan write-only groepen of volledig uitgesloten. Gebruik `#[Groups(['admin:read'])]` voor velden die alleen beheerders mogen zien, configureer dan de operatie-specifieke `normalizationContext` om die groep alleen bij admin-endpoints op te nemen. **V: Hoe werkt paginering in API Platform?** API Platform pagineert collections standaard met 30 items per pagina. De `page` query parameter regelt de offset. Hydra-metadata in JSON-LD-responses bevat `hydra:view` met first/last/next/previous-links. Configuratie via `paginationItemsPerPage`, `paginationMaximumItemsPerPage` en `paginationClientItemsPerPage` (staat clients toe verschillende paginagroottes aan te vragen). **V: Wanneer zou je een DTO gebruiken in plaats van de entity direct bloot te stellen?** DTO's ontkoppelen het API-contract van het databaseschema. Gebruik DTO's wanneer: (1) de API-representatie significant verschilt van de entity-structuur, (2) meerdere entities combineren tot één response, (3) berekende velden in responses verschijnen, (4) input-validatie verschilt van entity-constraints, of (5) de entity [Doctrine Inheritance](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html) gebruikt die serialisatie compliceert. DTO's voorkomen ook onbedoelde blootstelling van nieuwe entity-velden wanneer het schema verandert. ## API Platform Endpoints Testen API Platform werkt met Symfony's testframework. `ApiTestCase` biedt methoden voor geauthenticeerde requests en JSON-response assertions. ```php // tests/Api/ProductTest.php namespace App\Tests\Api; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use App\Entity\Product; class ProductTest extends ApiTestCase { public function testGetCollection(): void { $response = static::createClient()->request('GET', '/api/products'); $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains(['@context' => '/api/contexts/Product']); $this->assertMatchesResourceCollectionJsonSchema(Product::class); } public function testCreateProduct(): void { $response = static::createClient()->request('POST', '/api/products', [ 'json' => [ 'name' => 'Test Product', 'price' => '29.99' ], 'headers' => [ 'Authorization' => 'Bearer ' . $this->getAdminToken() ] ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ 'name' => 'Test Product', 'price' => '29.99' ]); } public function testCreateProductValidationFails(): void { static::createClient()->request('POST', '/api/products', [ 'json' => ['name' => 'AB'], // Te kort 'headers' => ['Authorization' => 'Bearer ' . $this->getAdminToken()] ]); $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ 'violations' => [ ['propertyPath' => 'name'] ] ]); } } ``` De methode `assertMatchesResourceCollectionJsonSchema()` valideert responses tegen het automatisch gegenereerde [JSON Schema](https://json-schema.org/), waardoor regressies worden gedetecteerd wanneer de entity-structuur verandert. ## Performance-optimalisatie met Eager Loading N+1-queries verslechteren de performance van collection endpoints. API Platform's `fetchEager`-optie en Doctrine's [Query Hints](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/dql-doctrine-query-language.html#query-hints) lossen dit op. ```php #[ApiResource( operations: [ new GetCollection( extraProperties: ['doctrine_orm_fetch_join' => true] ) ] )] #[ORM\Entity] class Order { #[ORM\ManyToOne(fetch: 'EAGER')] #[ORM\JoinColumn(nullable: false)] private Customer $customer; #[ORM\OneToMany(mappedBy: 'order', fetch: 'EAGER')] private Collection $items; } ``` Voor complexe queries presteren aangepaste State Providers met geoptimaliseerde DQL beter dan automatische eager loading. Meet het aantal queries met de Symfony Profiler voor en na optimalisatie. ## Productie-gereed API Platform Configuratie ```yaml # config/packages/api_platform.yaml api_platform: title: '%env(API_TITLE)%' version: '%env(API_VERSION)%' show_webby: false defaults: stateless: true cache_headers: max_age: 3600 shared_max_age: 3600 vary: ['Content-Type', 'Authorization'] extra_properties: standard_put: true exception_to_status: Symfony\Component\Security\Core\Exception\AccessDeniedException: 403 App\Exception\BusinessException: 400 ``` Schakel `show_webby` (de mascotte) uit in productie. De `standard_put`-eigenschap zorgt ervoor dat PUT resources volledig vervangt in plaats van samenvoegt, volgens [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.4)-semantiek. ## Belangrijkste Punten over API Platform in 2026 - State Providers halen gegevens op voor GET-requests. State Processors verwerken POST/PUT/PATCH/DELETE. Deze scheiding maakt schoon testen en aangepaste databronnen mogelijk. - Serialisatiegroepen bepalen welke velden in responses verschijnen. Gebruik verschillende groepen voor lijst- vs. detailweergaven, en write-only groepen voor wachtwoorden. - Filters voegen query parameters toe voor zoeken, sorteren en datumbereiken. Ingebouwde filters dekken de meeste gevallen, aangepaste filters behandelen complexe queries. - DTO's ontkoppelen API-contracten van databaseschema's. Gebruik ze wanneer de response-structuur verschilt van de entity of wanneer gegevens uit meerdere bronnen worden gecombineerd. - De [Symfony Serializer](/technologies/symfony/interview-questions/serializer) handelt de JSON-naar-entity conversie af. Begrip van zijn normalizers en context-opties is essentieel voor geavanceerd API Platform-gebruik. - API Platform 4.3 is de huidige stabiele release. Versie 5.0 (in alpha) vereist Symfony 7.4 of 8.0 en beëindigt ondersteuning voor oudere Symfony-versies. - Documentatie op [api-platform.com](https://api-platform.com/docs/) behandelt randgevallen die hier niet worden besproken. De [Doctrine ORM integratie](/blog/symfony/doctrine-orm-mastering-relationships) documentatie legt entity-relatieverwerking in detail uit. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/symfony/api-platform-symfony-rest-tutorial