From api-platform
Enables API Platform GraphQL support with operations, security, custom resolvers, and Relay pagination. Defaults to REST unless hard client requirement.
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-platform:graphqlThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **Default to REST.** GraphQL trades away things API Platform gives you for free
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.
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
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).
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.
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): ?objectQuery 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.
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.
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.
When a resource has mercure: true, GraphQL subscription operations push updates
through the Mercure hub — see mercure.
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:
composer require api-platform/graphql, then enable it in
config/api-platform.php under graphql ('enabled' => true) — not YAML. Depth/
complexity limits and graphiql live in the same config block.QueryItemResolverInterface /
QueryCollectionResolverInterface / MutationResolverInterface and are referenced
by class-string on the operation; Laravel's container resolves them — there are no
api_platform.graphql.*_resolver tags to apply.subscription operations depend on Mercure, which has no Laravel
integration (see mercure), so GraphQL subscriptions are effectively Symfony-only.Mutation/DeleteMutation has a namesecurity set on nested resources, not just the entry-point operationread: false set when the resolver fetches its own datapaginationType: 'page' set only if the client doesn't want Relay connectionsnpx claudepluginhub api-platform/skillset --plugin api-platformCovers GraphQL schema design, resolvers, DataLoader for N+1 prevention, federation, subscriptions, and client integration with Apollo/urql.
Builds GraphQL APIs with schema design, resolvers, DataLoader for N+1 prevention, error handling, and optimizations using Apollo Server (JS) or Graphene (Python). Useful for flexible queries, REST migrations, or subscriptions.
Guides GraphQL API development: schema design with Relay connections and payloads, thin resolvers, N+1 detection/fixes via DataLoader, subscriptions over WebSocket/Redis, federation, query depth/complexity limits.