GraphQL Directives
Composition works without intervention when independent subgraphs don't share types and fields. Composition does more than schema stitching: federation directives enable each subgraph to specify what it resolves and how it connects with other subgraphs in the federated graph. This lets the router expose a consistent whole that spans across subgraphs while requiring minimal coordination between teams.
directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
Restrict access to the annotated item to successfully authenticated users. For more granularity, use @requiresScopes.
directive @authorized(
arguments: InputValueSet
fields: InputValueSet
node: InputValueSet
metadata: _Any
) repeatable on OBJECT | INTERFACE | FIELD_DEFINITION
Use the authorized
directive to authorize resource access based on attribute or response data. Use this directive with the authorization hooks.
When you provide the arguments
in the directive, the router calls the authorize-edge-pre-execution hook and passes the selected argument values from the query input.
This example shows how to select the id
argument:
schema {
query: QueryRoot
}
type Query {
getUser(id: Int!): User
@authorized(arguments: "id", metadata: { accessLevel: "user" })
}
type User {
id: Int!
name: String!
}
When a query makes a getUser
edge request with the id 1
:
query {
getUser(id: 1) {
id
name
}
}
The query calls the authorize-edge-pre-execution hook with an edge-definition
of:
{
"parent_type_name": "Query",
"field_name": "getUser"
}
The arguments are:
{ "id": 1 }
The metadata contains:
{ "accessLevel": "user" }
Use the hook function with custom headers to verify whether to allow or deny the query.
When you provide the node
in the directive, the router calls the authorize-parent-edge-post-execution hook and passes the selected node values from the query result.
The following example shows you how to select the id
field in both locations: from the User
child node in the users
edge, and then from the User
parent node in the address
edge:
schema {
query: QueryRoot
}
type Query {
users: User @authorized(node: "id", metadata: { accessLevel: "user" })
}
type User {
id: Int!
name: String!
address: String!
@authorized(fields: "id", metadata: { accessLevel: "user:address" })
}
Query the getUser
edge:
query {
users {
id
name
address
}
}
The first hook that runs is authorize-edge-node-post-execution, which receives these arguments:
{
context: ..., // The context object
definition: {
parent_type_name: "Query",
field_name: "users",
},
nodes: [
'{"id": 1}',
'{"id": 2}',
],
metadata: '{"accessLevel": "user"}',
}
The second hook authorize-parent-edge-post-execution receives the following arguments:
{
context: ..., // The context object
definition: {
parent_type_name: "Query",
field_name: "users",
},
parents: [
'{"id": 1}',
'{"id": 2}',
],
metadata: '{"accessLevel": "user:address"}',
}
You can apply the directive to a node definition:
type User @authorized(metadata: { accessLevel: "user" }) {
id: Int! @join__field(graph: USERS)
name: String! @join__field(graph: USERS)
}
When you run a query to access the User
type:
query {
getUser(id: 1) {
id
name
}
}
The router will call the authorize-node-pre-execution hook with this node-definition
:
{
"type_name": "User"
}
The hook receives this metadata:
{ "accessLevel": "user" }
The hook function reads data from the custom header to the hook context objects and decides whether to allow responses that include User
type fields.
directive @composeDirective(name: String!) repeatable on SCHEMA
By default, composition only passes some built-in (@deprecated
) and federation directives (like @inaccessible
) from subgraphs into the federated schema.
Use the @composeDirective
directive to tell composition to keep instances of a specific directive in the final API schema of the Federated Graph.
directive @external on FIELD_DEFINITION | OBJECT
When used on a field, @external
indicates that the subgraph can't resolve the field even though it exists in the subgraph's schema. Use this directive only in combination with @provides
and @requires
.
When you apply @external
to an object, it has the same effect as applying @external
to each field in that object.
directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
The @inaccessible
directive excludes marked schema elements from the composed API schema that the gateway exposes. When you mark an item as @inaccessible
in any subgraph, the composition excludes it from the composed API, even when the same item appears without @inaccessible
in other subgraphs.
type PersonalDetails @inaccessible {
age: Int
heightCm: Int
birday: Date
}
type User @key(fields: "id") @key(fields: "socialSecurityNumber") {
id: ID!
# socialSecurityNumber is a key to enable fetching users by
# social security number, but we do not want the field to be
# queryable.
socialSecurityNumber: String! @inaccessible
# This field must be marked as inaccessible because the field
# type is inaccessible.
details: PersonalDetails @inaccessible
}
Let's say you share an RGB color type across multiple subgraphs. Because the type uses @shareable
, all subgraphs must return all fields. When you add a new field to the type, start by adding it to one subgraph and publishing.
This causes a composition error because only one subgraph contains the new field. You might choose to publish updates to all subgraphs quickly, though your federated graph won't compose during the interim.
While this works, it doesn't scale well with many subgraphs across different teams. Instead, use @inaccessible
for a better solution. Publish your first subgraph with the new field like this:
type Color @shareable {
red: Int!
green: Int!
blue: Int!
opacity: Int! @inaccessible
}
The inaccessible opacity
field won't trigger composition errors when other subgraphs don't define it. Add the field to other subgraphs one at a time. After all subgraphs include the field, remove @inaccessible
. All intermediate states will compose cleanly.
directive @interfaceObject on OBJECT
Federation supports entity interfaces, which follow the same model as regular entities but have different definition requirements and interface-specific behaviors.
An interface entity must include:
- An interface with a key in one subgraph.
- A regular object entity using the
@interfaceObject
directive in other subgraphs.
Objects that implement an entity interface automatically receive fields that other subgraphs contribute to that entity. This matches how objects normally implement regular interfaces.
directive @key(
fields: FieldSet!
resolvable: Boolean = true
) repeatable on OBJECT | INTERFACE
The @key
directive defines entities in your schema. An entity is a type that includes a key and appears in multiple subgraphs. It acts as the main mechanism to connect subgraphs in Federation, similar to a primary key.
When you create an entity type with the @key
directive, your Federation compatible GraphQL framework of choice requires you to define an entity resolver for that type. This resolver works with the Federation-specific Query._entities
field to fetch
Arguments:
fields
: A string that contains the GraphQL selection set for key fields. You can nest the selection (for example,@key(fields: "a { b } c d")
), but field arguments aren't valid (for example, don't use@key(fields: "id(type: UUID) { bytes }")
).resolvable
: Set this value to false to show that a subgraph references an entity (often by returning its key) but can't resolve it throughQuery._entities
. This means the subgraph doesn't have an entity resolver for that entity. Use this when a subgraph includes an entity key (likeauthor_id
on a blog post) but doesn't contribute fields to the entity.
This example uses a fictitious e-commerce website to show how entities work. Define the inventory
subgraph first:
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
itemsInStock: Int!
sku: String!
}
The second @key
means the inventory
subgraph resolves a Product
by using its SKU.
Then a reviews
subgraph for product reviews:
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]
}
The final search
subgraph finds products based on a user's search query:
type Query {
findProducts(searchQuery: String!): [Product!]
}
type Product @key(fields: "id") {
id: ID!
}
The federated graph's API looks like this after composition:
type Query {
findProducts(searchQuery: String!): [Product!]
}
type Product {
id: ID!
reviews: [Review!]
itemsInStock: Int!
sku: String!
}
API clients see one Product
type. Subgraphs contribute fields without requiring coordination.
directive @override(from: String!, label: String) on FIELD_DEFINITION
Use the @override
directive to migrate a field from one subgraph to another. To migrate a field to a new subgraph, first define it in the new subgraph. The rules of composition don't allow defining the same field in two subgraphs. If you define the field as @shareable
, all subgraphs must resolve it, not just the source and destination. Marking the new field as @inaccessible
doesn't help because you still can't switch the field between subgraphs without coordination and downtime.
The @override
directive solves this problem. When you add @override(from: "other-subgraph")
to a field, the gateway routes requests for that field to the subgraph with the override and ignores the field in other-subgraph
. Teams can deploy changes independently using this workflow (each bullet point represents a publish
):
- Deploy the new field with
@override
in the overriding subgraph. If the field doesn't work correctly in the new subgraph, reverse the change and the gateway will resume resolving the field in the original subgraph. - Remove the old field from the overridden subgraph.
- Remove the
@override
directive from the overriding subgraph.
Arguments:
from
: Specify the name of the subgraph that contains the field you want to override. Composition doesn't validate this name to support this workflow: 1. Define the overriding field with@override
, 2. Deploy, 3. Remove the overridden field, 4. Remove the@override
on the new field. This avoids breaking the migration at step 3 when subgraphs publish independently.label
: Controls partial or progressive overriding. Set thelabel
argument to a string formatted as "percent(n)" where n is an integer from 0-100. This percentage determines how much traffic the gateway routes to the overriding subgraph. For example,@override(from: "inventory", label: "percent(0)")
routes no traffic to the new subgraph, while@override(from: "inventory", label: "percent(100)")
behaves the same as@override(from: "inventory")
without thelabel
argument.
Split the comments handling from your blog engine monolith into a dedicated service with its own subgraph. The monolith defines the Post
type like this:
type Post @key(fields: "id") {
id: ID!
title: String
comments: [Comment!]
publishedAt: DateTime
author: User
}
To migrate the Post.comments
field to the new comments service, add the following to the comments subgraph's schema:
type Post @key(fields: "id") {
id: ID!
comments: [Comment!] @override(from: "monolith")
}
After you deploy this addition, the gateway routes all traffic for Post.comments
to the comments subgraph.
directive @provides(fields: FieldSet!) on FIELD_DEFINITION
The @provides
directive on a field tells the gateway that when resolving this field, the same subgraph can also resolve a set of other fields on that object to optimize performance.
You must annotate these other fields with @external
, since the subgraph can only resolve them when resolving the field with @provides
. Think of this directive as a more restricted version of @shareable
.
While shareable fields allow resolution at any time, fields marked with @external
and provided with @provides
only allow resolution when resolving their providing field.
The following shows a Farm
subgraph:
type Farm @key(fields: "id") {
id: ID!
name: String!
location: String
vegetables: [Vegetable] @provides(fields: "name")
}
type Vegetable @key(fields: "id") {
id: ID!
name: String! @external
}
extend type Query {
farm(id: ID!): Farm
vegetablesInSeason(date: Date!): [Vegetable!]
}
Here's the Vegetable
subgraph:
type Vegetable @key(fields: "id") {
id: ID!
name: String!
scientificName: String!
nutritionInfo: NutritionInfo
marketPriceEur: Int
}
Consider these two queries that demonstrate how @provides
works.
query {
farm(id: "6058691a-2d0a-47f1-95b3-1632f9ad16f9") {
id
name
}
}
The Query.farm
field provides name
, so the Farm
subgraph can resolve the whole query without contacting other subgraphs. However, in this second query:
query {
vegetablesInSeason(date: "2023-10-03") {
id
name
}
}
The gateway must fetch the vegetable name from the Vegetables
subgraph. API consumers see one unified Vegetable type that includes all fields defined across all subgraphs.
directive @requires(fields: FieldSet!) on FIELD_DEFINITION
The @requires
directive specifies when a field needs other fields from the parent type that other subgraphs can resolve. Here's an example: Consider a hotel booking subgraph that manages room service. This subgraph determines available room service based on a hotel's location and category, but doesn't store hotel information directly in its database.
The Hotels subgraph:
type Hotel @key(fields: "id") {
id: ID!
category: Int
countryCode: String
}
The RoomService subgraph:
type Hotel @key(fields: "id") {
id: ID!
category: Int @external
countryCode: String @external
roomServiceOffering: [String!]! @requires(fields: "category countryCode")
}
In this last snippet, the RoomService subgraph resolves Hotel.roomServiceOffering
but requires the category
and countryCode
fields from another subgraph. The @requires
directive indicates dependencies on category
and countryCode
fields, while @external
shows the subgraph can't resolve them directly. You must define the required fields on the type and annotate them with @external
.
When resolving a query that selects Hotel.roomServiceOffering
, the gateway queries the Hotels subgraph first before passing data to the RoomService subgraph to resolve roomServiceOffering
for that hotel. The gateway passes the retrieved fields to the entity resolver (Query._entities
) on the RoomService subgraph.
Other subgraphs can resolve fields marked with @external
. Zero subgraphs make the field with @requires
impossible to query, one subgraph works for regular entity fields, and multiple subgraphs work with @shareable
fields.
Use @requires
only on entity fields and always combine it with @external
.
directive @requiresScopes(
scopes: [[String!]!]!
) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
Users must have a matching scope
claim in their JWT access token to access the annotated item. Format the scope
claim as a space-separated string of scope names.
The directive's scopes
argument contains an array of arrays that defines combinations of scopes. Each inner array specifies a set of required scopes (AND logic). The outer array lists alternative scope combinations that can grant access (OR logic). You can list scopes in any order.
Let's restrict blog post view count access to users with both editor
and analytics
scopes, or users with admin scope:
type BlogPost {
id: ID!
title: String!
author: User
viewCount: Int @requiresScopes(scopes: [["admin"], ["editor", "analytics"]])
content: String
}
directive @shareable on FIELD_DEFINITION | OBJECT
Use this directive to share a type or field between subgraphs. In contrast to entities that use @key
, all subgraphs must resolve shareable types and fields. A Color type demonstrates this pattern:
type Color @shareable {
red: Int!
green: Int!
blue: Int!
}
Each subgraph that returns a Color
must provide all fields. Think of shareable types as value types that provide complete data.
When you annotate a type like Color
with @shareable
, it affects all fields of that type as if you added @shareable
to each field individually.