Develop your graph with Connectors
Use your graph to orchestrate REST API calls with Apollo Connectors and Apollo Federation
In the previous guide, you set up a graph in GraphOS Studio and started a local dev session. In this guide, you will develop your graph by connecting live REST API endpoints to your graph using Apollo Connectors.
Let's start developing!
🧰 Meet your tools
In this guide you'll be using the following Apollo tools:
Apollo Sandbox is a local development tool lets you test requests, inspect and debug responses, and more.
Apollo Connectors are the quickest way to set up a graph backed by REST endpoints.
Connectors are a part of the GraphOS Router, the single point of entry for clients to your graph.
None of these tools requires a paid plan.
Develop your graph
In this step, you will:
Walk through an example GraphQL schema to learn how Connectors declaratively integrate REST APIs.
Make requests in Apollo Sandbox to observe how Connectors orchestrate calls to the connected REST APIs.
Create more Connectors from scratch, where each Connector demonstrates a different capability of Apollo Connectors.
Love developer tooling? 🛠️
Explore example Connector
Let's take a look at the products.graphql
GraphQL schema to see how Connectors work.
The first few lines use
@link
directives to enable the latest versions of federation and Connectors.GraphQLproducts.graphqlextend schema @link( url: "https://specs.apollo.dev/federation/v2.10" import: ["@key"] ) @link( url: "https://specs.apollo.dev/connect/v0.1" import: ["@connect", "@source"] )
In the next few lines, the
@source
directive creates a reusable configuration that each Connector can reference. In this case, it only sets thebaseURL
to the ecommerce demo API this guide uses. You can also add request headers to a@source
.GraphQLproducts.graphql@source( name: "ecomm" http: { baseURL: "https://ecommerce.demo-api.apollo.dev/" } )
Product
is the standard object type the GraphQL API returns.GraphQLproducts.graphqltype Product { id: ID! name: String description: String }
The
Query
type has aproducts
field that the@connect
directive implements.What's the Query type?In a GraphQL schema, theQuery
type defines the entry points for reading data from a GraphQL schema. It acts as the root type that clients use to request specific fields and traverse the graph.GraphQLproducts.graphqltype Query { products: [Product] # A @connect directive defines the API data source of a GraphQL schema field. @connect( source: "ecomm" http: { GET: "/products" } selection: """ $.products { id name description } """ ) }
Here is what each of
@connect
's arguments does:source: "ecomm"
refers to the@source
instance.http
sets the method (GET
) and path (/products
) for the request.selection
maps the REST API's JSON response to the fields in the GraphQL schema.
To understand how to map a JSON response to your GraphQL schema, you inspect the JSON response. Here's what the response for /products
looks like:
/products
1{
2 "products": [
3 {
4 "id": 1,
5 "name": "Lunar Rover Wheels",
6 "createdAt": 1636742972000,
7 "updatedAt": 1636742972000,
8 "description": "Designed for traversing the rugged terrain of the moon, these wheels provide unrivaled traction and durability. Made from a lightweight composite, they ensure your rover is agile in challenging conditions.",
9 "slug": "lunar-rover-wheels",
10 "tags": [
11 {
12 "tagId": "space",
13 "name": "Space"
14 },
15 {
16 "tagId": "engineering",
17 "name": "Engineering"
18 },
19 {
20 "tagId": "rover",
21 "name": "Rover"
22 }
23 ],
24 "category": "Engineering Components",
25 "availability": "AVAILABLE"
26 },
27 {
28 "id": 2,
29 "name": "Zero-Gravity Moon Boots",
30 "createdAt": 1636742972000,
31 "updatedAt": 1636742972000,
32 "description": "Experience weightlessness with our Zero-Gravity Moon Boots! Specifically designed to provide comfort and support for lunar explorers, these boots are perfect for hopping around on the moon's surface.",
33 "slug": "zero-gravity-moon-boots",
34 "tags": [
35 {
36 "tagId": "space",
37 "name": "Space"
38 },
39 {
40 "tagId": "apparel",
41 "name": "Apparel"
42 },
43 {
44 "tagId": "moon",
45 "name": "Moon"
46 }
47 ],
48 "category": "Apparel",
49 "availability": "AVAILABLE"
50 },
51 {
52 "id": 3,
53 "name": "Asteroid Blaster Tool",
54 "createdAt": 1636742972000,
55 "updatedAt": 1636742972000,
56 "description": "A must-have tool for all space engineers! This high-powered blaster is designed for asteroid excavation and resource gathering, featuring an adjustable power setting for all your blasting needs.",
57 "slug": "asteroid-blaster-tool",
58 "tags": [
59 {
60 "tagId": "engineering",
61 "name": "Engineering"
62 },
63 {
64 "tagId": "tools",
65 "name": "Tools"
66 },
67 {
68 "tagId": "space",
69 "name": "Space"
70 }
71 ],
72 "category": "Engineering Tools",
73 "availability": "AVAILABLE"
74 },
75 {
76 "id": 4,
77 "name": "Interstellar Communication Device",
78 "createdAt": 1636742972000,
79 "updatedAt": 1636742972000,
80 "description": "Stay connected across the galaxies! Our Interstellar Communication Device allows you to send messages to fellow space travelers with secure encryption and planetary signal enhancement.",
81 "slug": "interstellar-communication-device",
82 "tags": [
83 {
84 "tagId": "tech",
85 "name": "Tech"
86 },
87 {
88 "tagId": "space",
89 "name": "Space"
90 },
91 {
92 "tagId": "communication",
93 "name": "Communication"
94 }
95 ],
96 "category": "Communication Devices",
97 "availability": "AVAILABLE"
98 },
99 {
100 "id": 5,
101 "name": "Galactic Navigation System",
102 "createdAt": 1636742972000,
103 "updatedAt": 1636742972000,
104 "description": "Never get lost in space again! Our Galactic Navigation System provides real-time location tracking, route planning, and hazard warnings to ensure safe travels through the cosmos.",
105 "slug": "galactic-navigation-system",
106 "tags": [
107 {
108 "tagId": "tech",
109 "name": "Tech"
110 },
111 {
112 "tagId": "navigation",
113 "name": "Navigation"
114 },
115 {
116 "tagId": "space",
117 "name": "Space"
118 }
119 ],
120 "category": "Navigation Devices",
121 "availability": "AVAILABLE"
122 },
123 {
124 "id": 6,
125 "name": "Mars Terrain Analyzer",
126 "createdAt": 1636742972000,
127 "updatedAt": 1636742972000,
128 "description": "A state-of-the-art device crafted for analyzing the soil and terrain of Mars. It provides scientists with crucial data about soil composition, moisture levels, and potential resources for future colonization.",
129 "slug": "mars-terrain-analyzer",
130 "tags": [
131 {
132 "tagId": "science",
133 "name": "Science"
134 },
135 {
136 "tagId": "analysis",
137 "name": "Analysis"
138 },
139 {
140 "tagId": "mars",
141 "name": "Mars"
142 }
143 ],
144 "category": "Scientific Devices",
145 "availability": "AVAILABLE"
146 },
147 {
148 "id": 7,
149 "name": "Comet Dust Collector",
150 "createdAt": 1636742972000,
151 "updatedAt": 1636742972000,
152 "description": "Gather dust from comets with this innovative collector! Designed for deep-space research, it captures samples with high efficiency while ensuring the integrity of the materials.",
153 "slug": "comet-dust-collector",
154 "tags": [
155 {
156 "tagId": "science",
157 "name": "Science"
158 },
159 {
160 "tagId": "research",
161 "name": "Research"
162 },
163 {
164 "tagId": "space",
165 "name": "Space"
166 }
167 ],
168 "category": "Research Equipment",
169 "availability": "AVAILABLE"
170 },
171 {
172 "id": 8,
173 "name": "Planetary Habitat Module",
174 "createdAt": 1636742972000,
175 "updatedAt": 1636742972000,
176 "description": "An essential for any space colonization mission! This habitat module provides a reliable, comfortable living environment for astronauts on planetary missions, complete with life support systems and smart technology.",
177 "slug": "planetary-habitat-module",
178 "tags": [
179 {
180 "tagId": "construction",
181 "name": "Construction"
182 },
183 {
184 "tagId": "space",
185 "name": "Space"
186 },
187 {
188 "tagId": "habitat",
189 "name": "Habitat"
190 }
191 ],
192 "category": "Living Spaces",
193 "availability": "AVAILABLE"
194 },
195 {
196 "id": 9,
197 "name": "Satellite Launch Pad Kit",
198 "createdAt": 1636742972000,
199 "updatedAt": 1636742972000,
200 "description": "Get your small satellites off the ground with our Satellite Launch Pad Kit! This kit includes everything you need to successfully launch mini satellites into orbit, including launch software and tracking system.",
201 "slug": "satellite-launch-pad-kit",
202 "tags": [
203 {
204 "tagId": "space",
205 "name": "Space"
206 },
207 {
208 "tagId": "launch",
209 "name": "Launch"
210 },
211 {
212 "tagId": "kit",
213 "name": "Kit"
214 }
215 ],
216 "category": "Launch Equipment",
217 "availability": "AVAILABLE"
218 },
219 {
220 "id": 10,
221 "name": "Planetary Resource Extractor",
222 "createdAt": 1636742972000,
223 "updatedAt": 1636842972000,
224 "description": "Tap into the resources of asteroids and moons! The Planetary Resource Extractor is engineered for durability and efficiency to help miners gather valuable materials from celestial bodies.",
225 "slug": "planetary-resource-extractor",
226 "tags": [
227 {
228 "tagId": "engineering",
229 "name": "Engineering"
230 },
231 {
232 "tagId": "mining",
233 "name": "Mining"
234 },
235 {
236 "tagId": "space",
237 "name": "Space"
238 }
239 ],
240 "category": "Mining Equipment",
241 "availability": "AVAILABLE"
242 }
243 ],
244 "summary": {
245 "total": 30
246 }
247}
Notice that the /products
endpoint returns many more fields than id
, name
, and description
. With Connectors (and for the sake of simplicity in this guide), you can select and return only the ones you need via selection mapping.
Selection mapping
In selection mapping, $
refers to the root of the response body. Since the list of products is nested under a products
key, the first part of the selection is $.products
. The product fields to map are wrapped within the $.products {}
object. Since the Product
type fields declared in the GraphQL schema map cleanly to product fields in the JSON response, the selection uses shorthand for field names.
These two selection mappings are equivalent:
selection: """
$.products {
id
name
description
}
selection: """
$.products {
id: id
name: name
description: description
}
What if schema field names are different from the JSON response?
name
field that you want to rename title
in your GraphQL API, you can map it like so:selection: """
$.products {
id,
title: name
description
}
Notice you can still use shorthand for any other field names that don't need to change.
Let's test things out by running a request in the Apollo Sandbox.
Run a request
Running rover dev
starts a local GraphQL server at http://localhost:4000
.
This is an instance of Apollo Sandbox. You can write operations and make a request to your GraphQL API in Apollo Sandbox, then use its tools to validate the response and debug issues.
In a browser, go to http://localhost:4000.
In the central Operation panel, copy and paste the following query to get all products.
GraphQLExample queryquery Products { products { id name description } }
Run the request by clicking the ▶️ Products button. Check the response.
You should see the products data you selected from the
/products
endpoint.
That's it—a fully functional GraphQL API built on Connectors! You'll create a second Connector from scratch. Before that, this guide offers a slight detour to showcase the Sandbox's Connectors Debugger. If you're eager to write your own Connector, skip to the next section and return here when you need it.
Explore Connectors debugger
Explore the Connectors debugger
Suppose the example Connector hadn't worked out of the box—for example, that the selection mapping was incorrect. You can use the Connectors Debugger in the Sandbox to figure out what's gone wrong.- Update your
products.graphql
so that the selection mapping is incorrect in the following way:GraphQLproducts.graphqltype Query { products: [Product] # A @connect directive defines the API data source of a GraphQL schema field. @connect( source: "ecomm" http: { GET: "/products" } selection: """ id name description """ ) }
Why is this incorrect?This selection mapping doesn't wrap the field names in$.products{ }
and therefore doesn't match the JSON response from/products
. It would match if that endpoint returned a top-level list of products that weren't nested under aproducts
key. - Save your
products.graphql
to hot reload the Sandbox. - Rerun the
Products
query by clicking the ▶️ Products button. Check the response.With the incorrect schema, the Response panel showsGraphQLExample queryquery Products { products { id name description } }
null
for the data returned.The Response panel also has a Connectors Debugger that can help determine what's gone wrong.
- Open it by clicking Response and selecting Connectors Debugger from the dropdown.
- You can see three mapping errors occurred, even though the HTTP response status code was
200
. - Clicking into the Mapping panel displays some helpful error messages:
Property .description not found in object
, etc. - The Response body panel of the debugger shows the raw JSON response and where the schema went wrong. The
/products
endpoint doesn't just return a list, it returns a list of products nested under theproducts
key. - Correct the selection mapping in
products.graphql
by reverting to the original schema, nesting the current selection inside of$.products{ }
.GraphQLproducts.graphqltype Query { products: [Product] # A @connect directive defines the API data source of a GraphQL schema field. @connect( source: "ecomm" http: { GET: "/products" } selection: """ $.products { id name description } """ ) }
- Save your changes to the
products.graphql
file in your editor. - Rerun your request by clicking the ▶️ Products button. The Connectors Debugger should now show no errors. Toggle the right panel to the Response to confirm the response is as expected.
Create a Connector with arguments
Next, you'll create your own Connector to interact with the /products/:id
endpoint.
Adding this Connector to your schema lets clients request a single product by ID from the same GraphQL API that lists all products.
Add a new
product
field to the rootQuery
type that requires an argument calledid
and returns aProduct
.GraphQLproducts.graphqltype Query { products: [Product] @connect( source: "ecomm" http: { GET: "/products" } selection: """ $.products { id name description } """ ) product(id: ID!): Product }
GraphQL argument syntaxIn a GraphQL schema, arguments are enclosed in parentheses that come after a field's name. Each argument consists of two parts: 1) the argument's name and 2) the type of data that will be passed as the argument's value. Learn more about GraphQL arguments.Next, use the
@connect
directive to connectQuery.product
to the/products/:id
endpoint. You can use arguments as path and query parameters via the$args
variable.GraphQLproducts.graphqltype Query { products: [Product] @connect(# -- snip -- #) product(id: ID!): Product @connect( source: "ecomm" http: { GET: "/products/{$args.id}" } ) }
Inspect the response body for this type of request to your REST API, for example,
/products/1
./products/1JSON{ "id": 1, "name": "Lunar Rover Wheels", "createdAt": 1675200000000, "updatedAt": 1675200000000, "description": "Innovatively designed wheels for lunar rovers, built to endure harsh moon terrain and provide optimal agility. Each wheel is constructed using advanced materials to withstand temperature fluctuations and dust.", "slug": "lunar-rover-wheels", "tags": [ { "tagId": "1", "name": "Instruments" }, { "tagId": "2", "name": "Space" } ], "category": "Engineering Components", "availability": "AVAILABLE", "variants": [ { "name": "Standard Wheel", "price": { "original": 4999, "discounts": [], "final": 4999 }, "specifications": { "Material": { "value": "Titanium alloy" }, "Diameter": { "value": "50 cm" } }, "inventory": { "quantity": 100, "sellUnavailable": false }, "shipping": { "ship1": { "weight": 5, "method": "GROUND", "estimate": { "price": 499, "arrival": 1675804800000 } }, "ship2": { "weight": 5, "method": "AIR", "estimate": { "price": 999, "arrival": 1675790400000 } } }, "upc": "0001234567890", "sku": "RW-001", "taxable": true, "variantId": "variant1" } ] }
Based on the response shape, complete the selection mapping. Since the response is a single object, you can directly map its fields to
Product
type fields without$
. See the mapping overview to learn more about how to use the mapping language.GraphQLproducts.graphqltype Query { products: [Product] @connect(# -- snip -- #) product(id: ID!): Product @connect( source: "ecomm" http: { GET: "/products/{$args.id}" } selection: """ id name description """ ) }
Save your
products.graphql
.
That's it—you've created a Connector from scratch!
Run a request
Run the following query in your Sandbox to get a single product by id:
query Product {
product(id: "1") {
id
name
description
}
}
You should see results for just a single product with id
1. You can update the id
to view results for other products.
The next Connector showcases how you can orchestrate multiple REST API calls with just one GraphQL request.
Orchestrate calls with Connectors
The ecommerce demo API has a /products/:id/reviews
endpoint to retrieve a particular product's reviews. Check out /products/1/reviews
for an example.
/products/1/reviews
{
"reviews": [
{
"id": 1,
"rating": 5,
"comment": "These wheels are perfect for my lunar exploration project!",
"author": "John D",
"createdAt": 1675200000000
},
{
"id": 2,
"rating": 4,
"comment": "Solid build quality, just wish they were a little lighter.",
"author": "Emma H",
"createdAt": 1675200000000
},
/// ...
],
"summary": {
"total": 10,
"averageRating": 4
}
}
This endpoint returns a list of reviews under the reviews
key, each with its own ID, numerical rating
, comment
, author
, and createdAt
timestamp. To display a product detail page with reviews, clients normally have to first query the /products/:id
endpoint for product details and then the /products/:id/reviews
for the product's reviews.
With Connectors, the client can make one request, asking for all the information at once. Behind the scenes, the graph is orchestrating the calls and combining the information into one client-friendly response. To define what the client response should look like, you need to update the GraphQL schema:
Add a new
Review
type underneath theProduct
type in yourproducts.graphql
schema.GraphQLproducts.graphqlextend schema # -- snip -- # type Product { id: ID! name: String description: String } type Review { id: ID! rating: Float comment: String }
Since one product has many reviews, you define that relationship by adding a
reviews
field to theProduct
type. It returns a list ofReview
objects.GraphQLproducts.graphqlextend schema # -- snip -- # type Product { id: ID! name: String description: String reviews: [Review] } type Review { id: ID! rating: Float! comment: String }
To retrieve a product's reviews, add a Connector to the
reviews
field in theProduct
type.GraphQLproducts.graphqlextend schema # -- snip -- # type Product { id: ID! name: String description: String reviews: [Review] @connect( source: "ecomm" http: { GET: "/products/{$this.id}/reviews" } selection: """ $.reviews { id rating comment } """ ) }
What's
$this
? Similar to the$args
variable you used to access arguments for the/products/{$args.id}
path when creating a Connector forQuery.product
, you can use$this
to access an object's fields when creating a Connector for a field on a root type likeProduct
.
That's it! Save your updated schema to see orchestration in action.
Run a request
Run a new query in your Sandbox to get a product's details and reviews:
query ProductWithRating {
product(id: "1") {
id
name
description
reviews {
id
rating
comment
}
}
}
You should see results for a single product, including its reviews. The Sandbox offers additional views in the right panel to help you understand what's happening behind the scenes.
Inspect a query plan
Click Response and select Query Plan to inspect a visual representation of the steps taken to resolve a request.
A query plan is created by the query planner, the part of the GraphOS Router that efficiently breaks down a GraphQL request into multiple, coordinated calls to various endpoints and services. Despite its name, the query planner handles all types of GraphQL operations—not just queries.
Because the request requires two calls to two separate endpoints, the query planner automatically sequences API calls (the two Fetch
nodes) and then combines the results (the Flatten
node).
The next Connector builds on the orchestration example by combining data from two endpoints about the same object type.
Enrich entities with Connectors
If you compare the product data returned by /products
and /products/1
, you'll notice that /products/:id
returns many more fields than /products
.
Show me the data
{
"id": 1,
"name": "Lunar Rover Wheels",
"createdAt": 1636742972000,
"updatedAt": 1636742972000,
"description": "Designed for traversing the rugged terrain of the moon, these wheels provide unrivaled traction and durability. Made from a lightweight composite, they ensure your rover is agile in challenging conditions.",
"slug": "lunar-rover-wheels",
"tags": [
{
"tagId": "space",
"name": "Space"
},
{
"tagId": "engineering",
"name": "Engineering"
},
{
"tagId": "rover",
"name": "Rover"
}
],
"category": "Engineering Components",
"availability": "AVAILABLE"
},
{
"id": 1,
"name": "Lunar Rover Wheels",
"createdAt": 1675200000000,
"updatedAt": 1675200000000,
"description": "Innovatively designed wheels for lunar rovers, built to endure harsh moon terrain and provide optimal agility. Each wheel is constructed using advanced materials to withstand temperature fluctuations and dust.",
"slug": "lunar-rover-wheels",
"tags": [
{
"tagId": "1",
"name": "Instruments"
},
{
"tagId": "2",
"name": "Space"
}
],
"category": "Engineering Components",
"availability": "AVAILABLE",
"variants": [
{
"variantId": "variant1",
"name": "Standard Wheel",
"price": {
"original": 4999,
"discounts": [],
"final": 4999
},
"specifications": {
"Material": {
"value": "Titanium alloy"
},
"Diameter": {
"value": "50 cm"
}
},
"inventory": {
"quantity": 100,
"sellUnavailable": false
},
"shipping": {
"ship1": {
"weight": 5,
"method": "GROUND",
"estimate": {
"price": 499,
"arrival": 1675804800000
}
},
"ship2": {
"weight": 5,
"method": "AIR",
"estimate": {
"price": 999,
"arrival": 1675790400000
}
}
},
"upc": "0001234567890",
"sku": "RW-001",
"taxable": true
}
]
}
This is a common pattern in REST APIs. Product listing pages let users browse multiple products at once, while product detail pages provide more comprehensive information about individual products.
Suppose your team wants to redesign the listing page to show swatches for different product variants, but that information is only available from the product detail endpoint. You can use Connectors to still make one request for the product listing page without having to change the underlying APIs. Specifically Connectors can coordinate the following tasks:
Calling the product listing endpoint (
/products
) to retrieve all productsCalling the product detail endpoints (
/products/:id
) to retrieve variant information for all productsCombining the data together into one response to the client
Apollo Federation uses entity types to represent a single object type that combines information from multiple data sources.
Working with entities
An entity is any object with data fields that can be fetched with one or more unique key fields, much like objects or rows in a database.
To define an entity type in a GraphQL schema, you use the @key
directive, followed by the unique identifier field(s).
An entity type must also specify how to retrieve the data for its fields. You can use Connectors to accomplish this.
Update your schema so that the
Product
type uses the@key
directive. The product'sid
field works well as its unique identifier.GraphQLproducts.graphqlextend schema # -- snip -- # type Product @key(fields: "id") { id: ID! # -- all the other fields -- # }
To the same
Product
type, add thevariants
field that's only available from the product detail endpoint. Then add the correspondingVariant
type.GraphQLproducts.graphqlextend schema # -- snip -- # type Product @key(fields: "id") { id: ID! # -- all the other fields -- # variants: [Variant] } type Variant { id: ID! name: String }
Add the
variants
field to your selection mapping for theQuery.product
Connector.GraphQLproducts.graphqltype Query { products: [Product] @connect(# -- snip -- #) product(id: ID!): Product @connect( source: "ecomm" http: { GET: "/products/{$args.id}" } selection: """ id name description variants { id: variantId name } """ ) }
noteSince/products/:id
returns variant IDs nested asvariant.variantId
, we need to rename the field to match the GraphQL schema.Finally, add
entity:true
to theQuery.product
Connector.GraphQLproducts.graphqltype Query { products: [Product] @connect(# -- snip -- #) product(id: ID!): Product @connect( source: "ecomm" http: { GET: "/products/{$args.id}" } selection: """ id name description variants { id: variantId name } """ entity: true ) }
The entity:true
part indicates that the Connector can use the unique key (the id
from the /products
endpoint) to merge the correct information together.
All together, these additions provide the necessary instructions for how to retrieve any product fields you want, whether you're writing a query for one product
or all products
.
Run a request
Run a new query in your Sandbox to get information for all products, including each product's variants
:
query ProductsWithVariants {
products {
id
name
description
variants {
id
name
}
}
}
In the right panel, you can see all product fields returned, including the variants
information, into a single response.
By checking out the Query Plan, you can see how Connectors are fetching data from two different endpoints and merging their data intelligently into the response.
Connectors summary
Before moving on to some additional capabilities of Rover and GraphOS, let's summarize the Connectors you've developed:
The existing example Connector fetches fields from the high-level
/products
endpoint.The argument Connector fetches fields from the
/products/:id
endpoint.The first orchestration Connector adds review data to products from the
/products/:id/reviews
endpoint.The entities Connector combines data from
/products
and/products/:id
endpoints for a more complete representation of theProduct
entity type.
Any combination of data from all these endpoints is available to all your clients with a single API request to the GraphQL API you've been developing locally. Next, you'll learn how to publish your updated schema to the graph registry in GraphOS to share your graph with your team.
Next steps
To share and collaborate on your updated graph with your team, you need to publish it to GraphOS. Learn how, and see what else GraphOS has to offer in next guide.