Apollo Federation subgraph specification
For implementing subgraphs in other languages
This content is provided for developers adding federated subgraph support to a GraphQL server library, and for anyone curious about the inner workings of federation. It is not required if you're already using a subgraph-compatible library like Apollo Server.
Servers that are partially or fully compatible with this specification are tracked in Apollo's subgraph compatibility repository.
To make a GraphQL service subgraph-capable, it needs the following:
Implementation of the federation schema specification
Support for fetching service capabilities
Implementation of stub type generation for references
Implementation of request resolving for entities.
Federation schema specification
Federated services will need to implement the following additions to the schema to allow the gateway to use the service for execution:
1scalar _Any
2scalar _FieldSet
3
4# a union of all types that use the @key directive
5union _Entity
6
7type _Service {
8 sdl: String
9}
10
11extend type Query {
12 _entities(representations: [_Any!]!): [_Entity]!
13 _service: _Service!
14}
15
16directive @external on FIELD_DEFINITION
17directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
18directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
19directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
20
21# this is an optional directive discussed below
22directive @extends on OBJECT | INTERFACE
For more information on these additions, see the glossary.
Fetch service capabilities
Schema composition at the gateway requires having each service's schema, annotated with its federation configuration. This information is fetched from each service using _service
, an enhanced introspection entry point added to the query root of each federated service.
Note that the
_service
field is not exposed by the gateway; it is solely for internal use.
The _service
resolver should return the _Service
type which has a single field called sdl
. This SDL (schema definition language) is a printed version of the service's schema including the annotations of federation directives. This SDL does not include the additions of the federation spec above. Given an input like this:
1extend type Query {
2 me: User
3}
4
5type User @key(fields: "id") {
6 id: ID!
7}
The generated SDL should match that exactly with no additions. It is important to preserve the type extensions and directive locations and to omit the federation types.
Some libraries such as graphql-java
don't have native support for type extensions in their printer. Apollo Federation supports using an @extends
directive in place of extend type
to annotate type references:
1type User @key(fields: "id") @extends {
2 id: ID! @external
3 reviews: [Review]
4}
Create stub types
Individual federated services should be runnable without having the entire graph present. Fields marked with @external
are declarations of fields that are defined in another service. All fields referred to in @key
, @requires
, and @provides
directives need to have corresponding @external
fields in the same service. This allows us to be explicit about dependencies on another service, and service composition will verify that an @external
field matches up with the original field definition, which can catch mistakes or migration issues (when the original field changes its type for example). @external
fields also give an individual service the type information it needs to validate and decode incoming representations (this is especially important for custom scalars), without requiring the composed graph schema to be available at runtime in each service.
A federated service should take the @external
fields and types and create them locally so the service can run on its own.
Resolve requests for entities
Execution of a federated graph requires being able to "enter" into a service at an entity type. To do this, federated services need to do two things:
Make each entity in the schema part of the
_Entity
unionImplement the
_entities
field on the query root
To implement the _Entity
union, each type annotated with @key
should be added to the _Entity
union. If no types are annotated with the key directive, then the _Entity
union and Query._entities
field should be removed from the schema. For example, given the following partial schema:
1type Review @key(fields: "id") {
2 id: ID!
3 body: String
4 author: User
5 product: Product
6}
7
8extend type User @key(fields: "email") {
9 email: String! @external
10}
11
12extend type Product @key(fields: "upc") {
13 upc: String! @external
14}
The _Entity
union for that partial schema should be the following:
1union _Entity = Review | User | Product
The _Entity
union is critical to support the _entities
root field:
1_entities(representations: [_Any!]!): [_Entity]!
Queries across service boundaries will start off from the _entities
root field. The resolver for this field receives a list of representations. A representation is a blob of data that is supposed to match the combined requirements of the fields requested on an entity.
For example, if we execute a query for the top product's reviews
:
1query GetTopProductReviews {
2 topProducts {
3 reviews {
4 body
5 }
6 }
7}
The gateway will first fetch the topProducts
from the Products service, asking for the upc
of each product:
1query {
2 topProducts {
3 upc
4 }
5}
The reason it requests upc
is because that field is specified as a requirement on reviews
:
1extend type Product @key(fields: "upc") {
2 upc: String @external
3 reviews: [Review]
4}
The gateway will then send a list of representations for the fetched products to the Reviews service:
1{
2 "query": ...,
3 "variables": {
4 "_representations": [
5 {
6 "__typename": "Product",
7 "upc": "B00005N5PF"
8 },
9 ...
10 ]
11 }
12}
1query ($_representations: [_Any!]!) {
2 _entities(representations: $_representations) {
3 ... on Product {
4 reviews {
5 body
6 }
7 }
8 }
9}
GraphQL execution will then go over each representation in the list, use the __typename
to match type conditions, build up a merged selection set, and execute it. Here, the inline fragment on Product
will match and the reviews
resolver will be called repeatedly with the representation for each product. Since Product
is part of the _Entity
union, it can be selected as a return of the _entities
resolver.
To ensure the required fields are provided, and of the right type, the source object properties should coerce the input into the expected type for the property (by calling parseValue on the scalar type).
The real resolver will then be able to access the required properties from the (partial) object:
1{
2 Product: {
3 reviews(object) {
4 return fetchReviewsForProductWithUPC(object.upc);
5 }
6 }
7}
Schema modifications glossary
type _Service
A new object type called _Service
must be created. This type must have an sdl: String!
field which exposes the SDL of the service's schema
Query._service
A new field must be added to the query root called _service
. This field must return a non-nullable _Service
type. The _service
field on the query root must return SDL which includes all of the service's types (after any non-federation transforms), as well as federation directive annotations on the fields and types. The federation schema modifications (i.e. new types and directive definitions) should not be included in this SDL.
union _Entity
A new union called _Entity
must be created. This should be a union of all types that use the @key
directive, including both types native to the schema and extended types.
scalar _Any
A new scalar called _Any
must be created. The _Any
scalar is used to pass representations of entities from external services into the root _entities
field for execution. Validation of the _Any
scalar is done by matching the __typename
and @external
fields defined in the schema.
scalar _FieldSet
A new scalar called _FieldSet
is a custom scalar type that is used to represent a set of fields. Grammatically, a field set is a selection set minus the braces. This means it can represent a single field "upc"
, multiple fields "id countryCode"
, and even nested selection sets "id organization { id }"
.
Query._entities
A new field must be added to the query root called _entities
. This field must return a non-nullable list of _Entity
types and have a single argument with an argument name of representations
and type [_Any!]!
(non-nullable list of non-nullable _Any
scalars). The _entities
field on the query root must allow a list of _Any
scalars which are "representations" of entities from external services. These representations should be validated with the following rules:
Any representation without a
__typename: String
field is invalid.Representations must contain at least the fields defined in the fieldset of a
@key
directive on the base type.
@key
1directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
The @key
directive is used to indicate a combination of fields that can be used to uniquely identify and fetch an object or interface.
1type Product @key(fields: "upc") {
2 upc: UPC!
3 name: String
4}
Multiple keys can be defined on a single object type:
1type Product @key(fields: "upc") @key(fields: "sku") {
2 upc: UPC!
3 sku: SKU!
4 name: String
5}
Note: Repeated directives (in this case,
@key
, used multiple times) require support by the underlying GraphQL implementation.
@provides
1directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
The @provides
directive is used to annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. Given the following example:
1type Review @key(fields: "id") {
2 product: Product @provides(fields: "name")
3}
4
5extend type Product @key(fields: "upc") {
6 upc: String @external
7 name: String @external
8}
When fetching Review.product
from the Reviews service, it is possible to request the name
with the expectation that the Reviews service can provide it when going from review to product. Product.name
is an external field on an external type which is why the local type extension of Product
and annotation of name
is required.
@requires
1directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
The @requires
directive is used to annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. For example:
1# extended from the Users service
2extend type User @key(fields: "id") {
3 id: ID! @external
4 email: String @external
5 reviews: [Review] @requires(fields: "email")
6}
In this case, the Reviews service adds new capabilities to the User
type by providing a list of reviews
related to a user. In order to fetch these reviews, the Reviews service needs to know the email
of the User
from the Users service in order to look up the reviews. This means the reviews
field / resolver requires the email
field from the base User
type.
@external
1directive @external on FIELD_DEFINITION
The @external
directive is used to mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. For example:
1# extended from the Users service
2extend type User @key(fields: "email") {
3 email: String @external
4 reviews: [Review]
5}
This type extension in the Reviews service extends the User
type from the Users service. It extends it for the purpose of adding a new field called reviews
, which returns a list of Review
s.