Graphql
Exposes API Platform resources over GraphQL — enabling GraphQL, Query/QueryCollection/Mutation/DeleteMutation operations, security expressions, custom resolvers, Relay cursor pagination, and nested relations. Use when the user mentions GraphQL, a GraphQL schema, queries/mutations, Relay connections, GraphQL playground, resolvers, or asks to expose existing REST resources via GraphQL.From its SKILL.md
npx -y skills add api-platform/skillset --skill graphqlAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
SKILL.md
7.2 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it
GraphQL
Default to REST. GraphQL trades away things API Platform gives you for free over REST: HTTP cache semantics (ETag, Cache-Control, invalidation), one URL per resource, simple CDN/proxy caching, and predictable per-operation cost. A single GraphQL query can fan out into arbitrarily deep/expensive resolution. Reach for GraphQL when it is a hard client requirement (e.g. a Relay/Apollo frontend, or clients that genuinely need to select fields and avoid round-trips) — not as a default. The same resource class can serve both; you don't have to choose globally.
Enabling GraphQL
Install api-platform/graphql (composer require api-platform/graphql), then it's
on. A /graphql endpoint and the GraphiQL playground (/graphql/graphiql) appear.
Disable globally or per resource as needed:
# config/packages/api_platform.yaml
api_platform:
graphql:
enabled: true
graphiql:
enabled: true
Declaring GraphQL operations
GraphQL operations live in graphQlOperations and are separate classes from the
REST ones, under ApiPlatform\Metadata\GraphQl\. A resource with no
graphQlOperations still gets a default set (item query, collection query, create /
update / delete mutations) once GraphQL is enabled.
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
#[ApiResource(graphQlOperations: [
new Query(),
new QueryCollection(),
new Mutation(name: 'create'),
new Mutation(name: 'update'),
new DeleteMutation(name: 'delete'),
])]
class Book {}
Mutation and DeleteMutation require a name — it becomes the GraphQL field
name (createBook, updateBook, deleteBook). Query and QueryCollection only
need a name when you declare more than one of the same kind (e.g. a custom query
alongside the default).
Security on GraphQL operations
Same ExpressionLanguage as REST (see operations), set per GraphQl operation:
new Query(security: "is_granted('ROLE_USER')")
new Mutation(name: 'update', security: "object.getOwner() == user")
new Mutation(name: 'update', securityPostDenormalize: "object.getOwner() == user")
Because one GraphQL query can traverse relations, securing only the top-level operation is not enough — guard the related resources' operations too, or a nested field becomes an unguarded read path.
Custom resolvers
Use a resolver when a query/mutation needs logic beyond fetch-by-id. Resolvers are services implementing one of:
QueryItemResolverInterface—__invoke(?object $item, array $context): objectQueryCollectionResolverInterface—__invoke(iterable $collection, array $context): iterableMutationResolverInterface—__invoke(?object $item, array $context): ?object
Query arguments arrive in $context['args'].
use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface;
final class BookResolver implements QueryItemResolverInterface
{
public function __invoke(?object $item, array $context): object
{
// $item is the fetched Book (or null if read: false); enrich or replace it
return $item;
}
}
With Symfony autoconfiguration the resolver is wired automatically. Without it, tag
the service api_platform.graphql.query_resolver (or ..._mutation_resolver). Then
reference it by class name on the operation, and set read: false when the
resolver should fetch the data itself rather than receiving a hydrated item:
new Query(name: 'recommended', resolver: BookResolver::class, read: false)
new Query(
name: 'search',
resolver: BookResolver::class,
args: [
'query' => ['type' => 'String!', 'description' => 'Full-text search'],
'limit' => ['type' => 'Int'],
],
)
args overrides the auto-generated argument set — define it when the query takes
parameters that aren't resource fields.
Relations and the N+1 trap
GraphQL embeds related resources by selecting nested fields:
{
book(id: "/books/1") {
title
author { name }
}
}
Relations resolve through the same providers as REST. A deeply nested query can trigger many small queries (the classic N+1). API Platform mitigates common cases, but verify against real queries and add Doctrine joins / a custom collection provider where a hot path fans out. This open-ended cost is the main reason REST is the safer default for cache-sensitive APIs.
Pagination: Relay cursor connections
Collection queries return Relay-style cursor connections by default
(edges { node { ... } cursor }, pageInfo, totalCount), driven by first/
after/last/before arguments:
{
books(first: 10, after: "endCursor") {
totalCount
edges { node { title } cursor }
pageInfo { endCursor hasNextPage }
}
}
To use simple page-based pagination instead, set paginationType: 'page' on the
resource or the QueryCollection:
use ApiPlatform\Metadata\GraphQl\QueryCollection;
#[ApiResource(graphQlOperations: [
new QueryCollection(paginationType: 'page'),
])]
class Book {}
All the pagination* controls from pagination (items per page, max, partial)
apply to GraphQL collections too.
Real-time subscriptions
When a resource has mercure: true, GraphQL subscription operations push updates
through the Mercure hub — see mercure.
Laravel
GraphQL is supported on Laravel. The operation classes (Query, QueryCollection,
Mutation, DeleteMutation), security expressions, Relay connections,
paginationType, custom resolvers and the N+1 caveats are all the same. Differences:
- Install
composer require api-platform/graphql, then enable it inconfig/api-platform.phpundergraphql('enabled' => true) — not YAML. Depth/ complexity limits andgraphiqllive in the same config block. - Resolvers implement the same
QueryItemResolverInterface/QueryCollectionResolverInterface/MutationResolverInterfaceand are referenced by class-string on the operation; Laravel's container resolves them — there are noapi_platform.graphql.*_resolvertags to apply. - Real-time
subscriptionoperations depend on Mercure, which has no Laravel integration (see mercure), so GraphQL subscriptions are effectively Symfony-only.
Checklist
- GraphQL chosen for a real client requirement, not as a REST default
- Every
Mutation/DeleteMutationhas aname -
securityset on nested resources, not just the entry-point operation - Custom resolvers tagged (or autoconfigured) and referenced by class name
-
read: falseset when the resolver fetches its own data - Deep/nested queries checked for N+1; joins added on hot paths
-
paginationType: 'page'set only if the client doesn't want Relay connections
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most apis services skills give in ~1.6k tokens
Counted across 448 of the 471 authors here whose files we hold, read 2026-09-06
- Use HTTP status codes semanticallyin 25 of 448, across 11 files
- Return 201 with a Location header on createin 24 of 448, across 9 files
- Name resources plural, lowercase, kebab-casein 23 of 448, across 9 files
- Configure rate limiting with limit headersin 22 of 448, across 8 files
- Paginate list endpoints with cursor or offsetin 21 of 448, across 10 files
- Version APIs in the URL pathin 21 of 448, across 11 files
- Validate request input with a schemain 21 of 448, across 7 files
- Add pagination to all list endpointsin 18 of 448, across 15 files
- Match HTTP method to the operationin 12 of 448, across 6 files
- Return 400 or 422 with field-level detailsin 12 of 448, across 2 files
- Check ownership before returning resourcesin 12 of 448, across 2 files
- Limit query depth and complexityin 12 of 448, across 7 files
Said here and by no other author read
- Default to REST; use GraphQL only when a client requires it
- Install api-platform/graphql with composer
- Give every Mutation and DeleteMutation a name
- Secure nested resource operations, not just the top-level operation
- Use custom resolvers for logic beyond fetch-by-id
- Reference resolvers by class name on the operation
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.