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.

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 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.
# API Platform installeren met Symfony Flex
composer require api
# Installatie verifiëren
php bin/console debug:router | grep apiDe api-recipe installeert api-platform/symfony samen met de serializer-, validator- en property-access-componenten. Symfony Flex configureert de routes automatisch onder /api.
# 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.
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.
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.
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
);
}
}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.
Klaar om je Symfony gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
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.
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:
#[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.
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.
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:
{
"@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 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.
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, 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 lossen dit op.
#[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
# 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: 400Schakel 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-semantiek.
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
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 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 behandelt randgevallen die hier niet worden besproken. De Doctrine ORM integratie documentatie legt entity-relatieverwerking in detail uit.
Zie jij de bug in Symfony?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 25 augustus 2026
Tags
Delen
Gerelateerde artikelen

Doctrine ORM: Relaties beheersen in Symfony
Volledige gids voor Doctrine ORM-relaties in Symfony. OneToMany, ManyToMany, laadstrategieën en performance-optimalisatie met praktische voorbeelden.

Symfony-sollicitatievragen: Top 25 in 2026
De 25 meest gestelde Symfony-sollicitatievragen. Architectuur, Doctrine ORM, services, beveiliging, formulieren en tests met gedetailleerde antwoorden en codevoorbeelden.

Symfony 7: API Platform en Best Practices
Volledige gids voor het bouwen van professionele REST API's met Symfony 7 en API Platform 4. State Providers, Processors, validatie en serialisatie uitgelegd met praktische voorbeelden.