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.

API Platform GraphQL transforms Symfony applications into powerful, type-safe APIs that solve over-fetching and under-fetching problems inherent to REST. With automatic schema generation from PHP attributes and full Relay specification support, API Platform 4.x provides a production-ready GraphQL implementation that requires minimal configuration.
GraphQL requests exactly the fields needed in a single query, while REST returns fixed response structures. API Platform generates both endpoints from the same resource definition, letting clients choose the protocol that fits their use case.
Installing and Enabling GraphQL Support in Symfony
API Platform separates GraphQL functionality into a dedicated package. This modular approach keeps the core lightweight for projects that only need REST.
# Install GraphQL support
composer require api-platform/graphqlOnce installed, the /graphql endpoint becomes available automatically. The schema generates from existing #[ApiResource] attributes without additional configuration.
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\GraphQl\Mutation;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ApiResource(
graphQlOperations: [
new Query(),
new QueryCollection(),
new Mutation(name: 'create'),
new Mutation(name: 'update'),
new Mutation(name: 'delete'),
]
)]
class Book
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(length: 13)]
private string $isbn;
#[ORM\Column]
private \DateTimeImmutable $publishedAt;
// Getters and setters...
}This single entity definition exposes queries for fetching books by ID or as collections, plus mutations for create, update, and delete operations. The GraphQL schema reflects PHP types directly: string becomes String!, nullable types become optional fields.
Writing GraphQL Queries and Mutations
GraphQL queries specify exactly which fields to return. This precision eliminates wasted bandwidth and reduces client-side data transformation.
# Fetch a single book with specific fields
query GetBook {
book(id: "/books/42") {
title
isbn
publishedAt
}
}
# Fetch a collection with pagination
query ListBooks {
books(first: 10, after: "cursor123") {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}Mutations follow the Relay specification with input objects and clientMutationId for request tracking.
# Create a new book
mutation CreateBook {
createBook(input: {
title: "Domain-Driven Design"
isbn: "9780321125217"
publishedAt: "2003-08-30"
clientMutationId: "create-1"
}) {
book {
id
title
}
clientMutationId
}
}
# Update an existing book
mutation UpdateBook {
updateBook(input: {
id: "/books/42"
title: "Updated Title"
clientMutationId: "update-1"
}) {
book {
id
title
}
}
}The clientMutationId helps clients correlate responses with requests in batch scenarios. API Platform returns it unchanged in the response.
Implementing Custom Resolvers for Complex Business Logic
Default CRUD operations cover basic cases, but real applications need custom business logic. API Platform provides resolver interfaces for queries and mutations.
namespace App\Resolver;
use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface;
use App\Repository\BookRepository;
final class BookBestSellerResolver implements QueryCollectionResolverInterface
{
public function __construct(
private readonly BookRepository $bookRepository
) {}
/**
* @param iterable<Book> $collection
* @return iterable<Book>
*/
public function __invoke(iterable $collection, array $context): iterable
{
// Access GraphQL arguments from context
$limit = $context['args']['limit'] ?? 10;
$period = $context['args']['period'] ?? 'month';
return $this->bookRepository->findBestSellers($limit, $period);
}
}Register the custom resolver in the entity configuration:
#[ApiResource(
graphQlOperations: [
new QueryCollection(
name: 'bestSellers',
resolver: BookBestSellerResolver::class,
args: [
'limit' => ['type' => 'Int', 'default_value' => 10],
'period' => ['type' => 'String', 'default_value' => 'month'],
]
),
]
)]
class Book
{
// ...
}This exposes a bestSellers query that accepts limit and period arguments, executing custom repository logic instead of default Doctrine queries.
Ready to ace your Symfony interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Securing GraphQL Operations with Voters and Expressions
Security configuration for GraphQL operates independently from REST. Each operation can define its own access rules using Symfony's expression language.
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\Mutation;
#[ApiResource(
graphQlOperations: [
new Query(
security: "is_granted('ROLE_USER')"
),
new QueryCollection(
security: "is_granted('ROLE_USER')"
),
new Mutation(
name: 'create',
security: "is_granted('ROLE_EDITOR')"
),
new Mutation(
name: 'update',
security: "is_granted('ROLE_EDITOR') and object.getAuthor() == user",
securityMessage: "Only the author can update this book."
),
new Mutation(
name: 'delete',
security: "is_granted('ROLE_ADMIN')"
),
]
)]
class Book
{
// ...
}The object variable in security expressions refers to the entity being accessed. This enables fine-grained ownership checks. For complex authorization logic, Symfony Security voters provide a cleaner solution than inline expressions.
namespace App\Security\Voter;
use App\Entity\Book;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class BookVoter extends Voter
{
public const EDIT = 'BOOK_EDIT';
public const DELETE = 'BOOK_DELETE';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::DELETE])
&& $subject instanceof Book;
}
protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token
): bool {
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
/** @var Book $book */
$book = $subject;
return match($attribute) {
self::EDIT => $book->getAuthor() === $user,
self::DELETE => in_array('ROLE_ADMIN', $user->getRoles()),
default => false,
};
}
}Real-Time Updates with GraphQL Subscriptions
API Platform implements GraphQL subscriptions through Mercure, a protocol for server-sent events. Subscriptions push data to clients when resources change.
use ApiPlatform\Metadata\GraphQl\Subscription;
#[ApiResource(
mercure: true,
graphQlOperations: [
new Query(),
new Mutation(name: 'update'),
new Subscription(),
]
)]
class Book
{
// ...
}Clients subscribe to changes using standard GraphQL subscription syntax:
subscription BookUpdates {
updateBookSubscribe(input: { id: "/books/42" }) {
book {
id
title
updatedAt
}
}
}When a mutation updates the book, Mercure broadcasts the change to all subscribed clients. This pattern fits collaborative applications, live dashboards, and real-time notifications. For high-throughput scenarios, combine subscriptions with Symfony Messenger to decouple mutation processing from notification dispatch.
Interview Questions: API Platform GraphQL
Technical interviews for Symfony positions increasingly cover GraphQL integration. These questions test understanding of both the specification and API Platform's implementation.
Q: How does API Platform generate the GraphQL schema?
API Platform introspects #[ApiResource] attributes and PHP type declarations to build the schema. Entity properties become fields, with PHP types mapping to GraphQL types. The schema regenerates on each request in development mode and caches in production.
Q: What is the difference between Query and QueryCollection operations?
Query fetches a single item by identifier and requires an id argument. QueryCollection returns multiple items with optional filtering, pagination, and sorting. Both can have custom resolvers, but their interfaces differ: QueryItemResolverInterface vs QueryCollectionResolverInterface.
Q: How do you handle N+1 query problems in API Platform GraphQL?
DataLoader pattern batches multiple database queries into one. API Platform integrates with Doctrine's eager loading through fetch joins in the query extension. For complex cases, implement a custom resolver that uses Doctrine's addSelect() to fetch associations in the initial query.
// Custom query extension for eager loading
public function applyToCollection(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
$queryBuilder
->addSelect('author')
->leftJoin('o.author', 'author');
}Q: Can REST and GraphQL security rules differ for the same resource?
Yes. REST operations use security on #[Get], #[Post], etc., while GraphQL operations use security on #[Query], #[Mutation], etc. This separation allows stricter rules for one protocol. A common pattern exposes read-only GraphQL for public clients while REST mutations require authentication.
Q: How do you add custom scalar types to the GraphQL schema?
Register a custom type in config/packages/api_platform.yaml and implement the serialization logic:
# config/packages/api_platform.yaml
api_platform:
graphql:
enabled: true
graphiql:
enabled: truenamespace App\GraphQL\Type;
use GraphQL\Type\Definition\ScalarType;
final class DateTimeType extends ScalarType
{
public string $name = 'DateTime';
public function serialize($value): string
{
return $value->format(\DateTimeInterface::RFC3339);
}
public function parseValue($value): \DateTimeImmutable
{
return new \DateTimeImmutable($value);
}
public function parseLiteral($valueNode, ?array $variables = null): \DateTimeImmutable
{
return new \DateTimeImmutable($valueNode->value);
}
}Pagination Strategies: Cursor vs Page-Based
API Platform defaults to cursor-based pagination following the Relay Connection specification. This approach handles real-time data better than offset pagination because insertions do not shift results.
# Cursor-based (default)
query {
books(first: 10, after: "YXJyYXljb25uZWN0aW9uOjk=") {
edges {
cursor
node {
title
}
}
pageInfo {
endCursor
hasNextPage
}
}
}Page-based pagination suits simpler use cases where clients need direct page access:
// Enable page-based pagination
#[ApiResource(
paginationType: 'page',
graphQlOperations: [
new QueryCollection(paginationType: 'page'),
]
)]
class Book {}# Page-based
query {
books(page: 2, itemsPerPage: 20) {
collection {
title
}
paginationInfo {
totalCount
lastPage
}
}
}Cursor pagination performs better at scale because it avoids OFFSET queries. The tradeoff is that clients cannot jump to arbitrary pages.
Testing GraphQL Endpoints in Symfony
Functional tests verify GraphQL behavior using Symfony's test client. API Platform provides a GraphQL-specific test trait.
namespace App\Tests\GraphQL;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;
class BookTest extends ApiTestCase
{
public function testQueryBook(): void
{
$client = static::createClient();
// Create test data
$book = new Book();
$book->setTitle('Test Book');
$book->setIsbn('1234567890123');
$book->setPublishedAt(new \DateTimeImmutable());
$em = static::getContainer()->get('doctrine')->getManager();
$em->persist($book);
$em->flush();
// Execute GraphQL query
$response = $client->request('POST', '/graphql', [
'json' => [
'query' => '
query GetBook($id: ID!) {
book(id: $id) {
title
isbn
}
}
',
'variables' => [
'id' => '/books/' . $book->getId(),
],
],
]);
$this->assertResponseIsSuccessful();
$data = $response->toArray();
$this->assertEquals('Test Book', $data['data']['book']['title']);
$this->assertEquals('1234567890123', $data['data']['book']['isbn']);
}
public function testMutationRequiresAuthentication(): void
{
$client = static::createClient();
$response = $client->request('POST', '/graphql', [
'json' => [
'query' => '
mutation CreateBook {
createBook(input: {
title: "Unauthorized Book"
isbn: "0000000000000"
clientMutationId: "test"
}) {
book { id }
}
}
',
],
]);
$data = $response->toArray();
$this->assertArrayHasKey('errors', $data);
}
}These tests validate both successful operations and security enforcement. Run them with php bin/phpunit tests/GraphQL/ to catch regressions in API behavior.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Key Takeaways for API Platform GraphQL in Symfony
- Install
api-platform/graphqlto enable the/graphqlendpoint with automatic schema generation from#[ApiResource]attributes - Use
Query,QueryCollection, andMutationoperations to control which GraphQL operations each resource exposes - Implement
QueryItemResolverInterfaceorMutationResolverInterfacefor custom business logic beyond CRUD - Security expressions on GraphQL operations work independently from REST, allowing different access rules per protocol
- Enable Mercure for real-time subscriptions that push changes to clients when resources update
- Cursor-based pagination handles real-time data better than page-based, but sacrifices direct page access
- Test GraphQL endpoints with
ApiTestCaseand JSON POST requests to/graphql - Interview questions focus on schema generation, N+1 problems, security separation, and custom resolvers
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 August 27, 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 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.

Symfony Testing in 2026: PHPUnit, KernelTestCase and Functional Tests
Master Symfony testing with PHPUnit 12, KernelTestCase for integration tests, and WebTestCase for functional testing. Learn database isolation, authentication testing, and code coverage configuration.