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 through Query._entities. This means the subgraph doesn't have an entity resolver for that entity. Use this when a subgraph includes an entity key (like author_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 the label 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 the label 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.