Working with Entities
Use Apollo Connectors to enrich and reference entities using REST APIs
Apollo Connectors are designed to simplify working with entities in federated graphs. An entity is any object that can identified with one or more unique key fields, much like rows in a database table. The different data fields are like columns in the table. GraphQL federation uses entity types to represent object types that combine information from multiple data sources.
This guide shows some common patterns for working with entities via connectors. Before diving into example connectors, it's helpful to understand a little about how to identify entities.
Identifying entities
Most APIs associate entities with particular business domains.
When working with REST APIs, if there's a GET
endpoint with a unique identifier (for example, /products/:id
) it's likely you're dealing with an entity.
Combining endpoints to complete an entity
Connectors can orchestrate multiple endpoints to provide a unified representation of a type.
For example, the connectors quickstart shows how to create a connector that combines product data from a /products
endpoint that provides a few fields for all products and a /products/:id
endpoint that provides more fields, specifically product variant information, for a single product.
type Query {
products: [Product]
@connect(
source: "ecomm"
http: { GET: "/products" }
selection: """
$. products {
id
name
description
}
"""
)
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
selection: """
id
name
description
variants {
id: variantID
name
}
"""
entity: true
)
}
type Product {
id: ID!
name: String
description: String
variants: [Variant]
}
type Variant {
id: ID!
name: String
}
One important detail is marking the /products/:id
connector with entity: true
.
This indicates that this endpoint can fetch additional fields for individual product entities. These entities are fetched using a unique key (the id
field the /products
endpoint returns, in this case).
How does the router handle this query?
query ListProductsAndVariants {
products {
id
name
variants {
id
name
}
}
}
Combining representations of the same type with multiple connectors
You can add multiple connectors to the same field, and the GraphOS Router chooses to call one or both depending on the fields in the client query. This is especially useful when you have multiple API versions.
type Query {
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/v1/products/{$args.id}" }
selection: """
id
color
}
"""
entity: true
)
@connect(
source: "ecomm"
http: { GET: "/v2/products/{$args.id}" }
selection: """
id
name
price
variants {
id: variantID
color
}
"""
entity: true
)
}
type Product {
id: ID!
name: String
price: Int
color: String @deprecated(reason: "Use the 'variants' field instead")
variants: [Variant]
}
type Variant {
id: ID!
color: String
}
Efficiently fetching additional information
If you provide the GraphOS Router with multiple connectors that fetch additional information, it can choose the optimal endpoint to resolve the requested fields.
For example, if the client operation requests the reviews
field, the GraphOS Router chooses the second connector to fetch the reviews along with the product details.
type Query {
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
selection: """
id
name
price
"""
entity: true
)
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}?include=reviews" }
selection: """
id
name
price
reviews {
id
rating
comment
}
"""
entity: true
)
}
type Product {
id: ID!
name: String
price: Int
reviews: [Review]
}
type Review {
id: ID!
rating: Float
comment: String
}
Working across subgraphs
Connectors work seamlessly to combine data across multiple services, or subgraphs, into a unified GraphQL API, or supergraph. You can use connectors to contribute to and reference entities from other subgraphs, whether those are different REST or GraphQL APIs.
Working across subgraphs with keys
As your schema grows, it's common to maintain each subgraph schema in its own schema file, for example, products.graphql
, reviews.graphql
, etc. Modularizing schemas likes this improves collaboration by allowing independent development and deployment across teams.
To identify an entity across different subgraphs, GraphQL schemas rely on the @key
directive. The @key
directive defines which key field(s) to cross-identify a particular entity type.
For example, a Product
type may be uniquely identifiable by a SKU or ID.
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}
Contributing entity fields across subgraphs
Any number of different subgraphs can contribute fields to an entity. For example, you may have both a products subgraph and a reviews subgraph contributing fields to a Product entity.
The products subgraph may provide product data via a connector that orchestrates /products/
and /products/:id
endpoints.
type Query {
products: [Product]
@connect(
source: "ecomm"
http: { GET: "/products" }
selection: """
$. products {
id
name
}
"""
)
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
selection: """
id
description
"""
entity: true
)
}
type Product {
id: ID!
title: String
description: String
}
Product review data may come from a separate review subgraph that connects a /reviews?product_id=:productId
endpoint to your graph.
type Product @key(fields: "id") {
id: ID!
reviews: [Review]
@connect(
source: "example"
http: { GET: "/reviews?product_id={$this.id}" }
selection: """
$.reviews {
id
title
body
rating
}
"""
)
}
type Review {
id: ID!
title: String
body: String
rating: Int
}
The @connect
directive defines the reviews
fields to request from the /reviews
endpoint.
The id
used as a parameter in the endpoint URL comes from the product's id
field, which comes from the products subgraph.
Referencing entities across subgraphs
If your REST API provides foreign key references, you can use them to reference entities and fetch corresponding fields from a different subgraph.
For example, if your REST API lets you fetch a user's favorite products and provides those products' IDs, you can use the @connect
directive to reference the Product
entity:
type User @key(fields: "id") {
id: ID!
favoriteProducts: [Product]
@connect(
source: "example"
http: { GET: "/users/{$this.id}/favorites" }
selection: """
$.products {
id: productId
}
"""
)
}
type Product @key(fields: "id", resolvable: false) {
id: ID!
}
The mapping language maps the productId
to the Product
entity's id
field.
The resolvable: false
argument denotes that while this subgraph includes a definition for the Product
entity, it doesn't provide a way to fetch or resolve all of it's fields. Learn more.
Adding a computed field using @requires
You might need to compute a field's value based on data from other subgraphs that contribute to an entity type.
The @requires
directive allows a subgraph to declare that it depends on specific fields from the same entity before resolving a field. The @external
directive declares that a field is resolved from a different subgraph.
1type Product @key(fields: "id") {
2 id: ID!
3 weight: Int @external
4 shippingCost(zipCode: String): Int
5 @requires(fields: "weight")
6 @connect(
7 source: "example"
8 http: {
9 GET: "/shipping?zip={$args.zipCode}&weight={$this.weight}"
10 }
11 selection: "$.result"
12 )
13}
In this example, shippingCost
depends on the weight
field.
Because weight
is marked as @external
(coming from another subgraph), the @requires
directive ensures that it is included in the query plan before resolving shippingCost
.
Compatibility with other federation features
Connectors work seamlessly with many other federation features. You can use directives like @tag
, @inaccessible
, @provides
, and more alongside @connect
.
See the limitations reference for a list of unsupported federation directives.
Additional resources
See the entity documentation for additional information on using entities in GraphQL Federation.
Check out the Thinking in Entities blog post for best practices for using entities with connectors.