API Orchestration with Connectors – Thinking in Entities
![Michael Watson](https://wp.apollographql.com/wp-content/uploads/2020/06/watson-dd5ebe7f9b6c2cd718f51d19c5fd4469-150x150.jpg)
Michael Watson
![API Orchestration with Connectors - Thinking in Entities](https://wp.apollographql.com/wp-content/uploads/2025/02/Thinking-in-Entities.png)
Since we first announced Apollo Connectors for REST at GraphQL Summit 2024, there has been an enormous amount of excitement from the community to dive in. I’ve been working with dozens of Enterprises on their use cases from breaking apart existing monoliths to integrating multiple production OpenAPI specification endpoints. In my work, there have been some patterns that have emerged and I wanted to start a new blog series that helps share the happy path to building faster. One of the most common questions I get is how do I connect this thing to that thing.
But before we begin connecting things together, let’s dive a bit into entities. We all have entities in our APIs that are associated with the domains of our business and identifying them at the start is a quick way to get on the right schema design pattern.
Identifying Entities
Understanding which objects in our APIs are entities might seem trivial to some, but it can get complicated when you have a service catalog of 100s or 1000s of APIs. For the purposes of our schema, it’s great to start looking at endpoints that have unique identifiers for the type. Taking a look at the PetStore swagger from editor.swagger.io, we can see that Pet
is an entity:
![](https://wp.apollographql.com/wp-content/uploads/2025/02/Screenshot-2025-02-04-at-4.01.27-PM.png)
The hints we can see to help define entities are:
- GET route
- URL structure has unique identifier in it (i.e. /pet/{petID})
At the same time, you can see that Store
doesn’t have a unique identifier available in any of the listed routes. So Store
isn’t an entity that we would want to expose (although that could just be an error in the OpenAPI spec because Store
seems like it should be uniquely identifiable, but if I had a nickel for every time I found an incomplete OpenAPI spec I wouldn’t need to have a job 😂).
You might also have entities that are uniquely identifiable by multiple parameters in the URL structure and that is perfectly fine. You’ll want to express these entities in your schema as they have already been defined. There is probably multiple good reasons for why that entity is defined in the given domain and we’re not here to rebuild, we’re here to connect.
You’ve already invested a lot in your existing REST APIs, they’re not technical debt, they’re technical assets and you just need a better way to unlock them for developers.
Exposing an Entity in a Connector
Now that we’ve identified some entities, let’s look at how we can connect them. We’ll use a Product
entity for this example:
// GET /products/:product_id
{
"id": "prod_123",
"name": "OG Olive Putter - Blade",
"description": "The traditional Block in a blade shape is made from a solid block of Olive wood. The head weight is approximately 360 grams with the addition of pure tungsten weights. Paired with a walnut center-line and white accents colors.",
"image": "https://keynote-strapi-production.up.railway.app/uploads/thumbnail_IMG_9102_3119483fac.png",
"price_id": "price_123"
}
First we need to design the schema that should be our Product
entity and we should keep that structure as is. The only exception is that we should avoid exposing anything that is an “entityId” in our schema – this is an anti-pattern. In our example, we shouldn’t expose price_id
but express the Price
as another object in our schema:
type Product {
id: ID!
name: String
description: String
image: String
price: Price
}
type Price {
id: ID!
}
Price
surely has additional fields, but we’ll get to that when we want to connect the two entities together. First we need to connect Product
to our endpoint which we can do by exposing it on the root Query
. This requires us to also create a selection
from the JSON response of our API:
type Query {
product(id:ID!): Product
@connect(
source: "products"
http: { GET: "/products/{$args.id}" }
selection: """
id
name
description
image
price: {
id: price_id
}
"""
entity: true
)
}
We can take the id
argument from the Query.product
field and append it to our URL structure using $args
. Notice that we actually create a new object in our selection for the price
field. This is done by adding the “:” to price
because the price_id
is returned in the flat structure of our JSON response. We then map the id
field to the price_id
.
Finally we marked the @connect
with entity: true
to mark this as an entity. This enables us to build other connectors (or GraphQL servers) that can use Product
and our execution will utilize this root entry point automatically when additional fields of a Product
are needed.
As a general rule of thumb, if you see “entityId” or “entity_id” in any of your JSON responses, you should expand that into another object or omit it from your schema and selection. Expanding it can be generalized to:
entity: {
id: entityId
}
Price
to our Product
Connecting We already created our Price
object, but we need to add the additional fields that are exposed in our pricing endpoint:
// GET /prices/:price_id
{
"id": "price_123",
"currency": "usd",
"unit_amount": 499.99
}
The challenge here is that we have a price endpoint that requires us to use the id of the Price
entity to get the details. This is where the magic 🪄of connectors comes in for me. If we simply expose our Price
as an entity like we did Product
, our orchestration will just work automatically. So let’s copy the same pattern we did, but we don’t need to necessarily make Price
an entity. We don’t have a use case to connect another entity to the Price
so we can omit entity: true
for now:
type Product {
id: ID!
name: String
description: String
image: String
price: Price
}
type Price {
id: ID!
unit: String
amount: Float
}
type Query {
price(id:ID!): Price
@connect(
source: "products"
http: { GET: "/prices/{$args.id}" }
selection: """
id
unit
amount: unit_amount
"""
)
product(id:ID!): Product
@connect(
source: "products"
http: { GET: "/products/{$args.id}" }
selection: """
id
name
description
image
price: {
id: price_id
}
"""
entity: true
)
}
Now if someone queries a Product
, our graph router will be able to understand these two connections can be brought together because the /products/:product_id
endpoint returns the Price.id
in the selection set. And it all just works together like magic 🌈
![](https://wp.apollographql.com/wp-content/uploads/2025/02/Screenshot-2025-02-04-at-6.49.22-PM-1024x576.png)
This is an extremely powerful orchestration of our API endpoints and a pattern you can repeat for any URL structure that follows:
/foo/:foo_id
– where the API response containsbar_id
/bar/:bar_id
But you could also have a different API structure where that pricing information is actually contained in the product url: /products/:product_id/price
. In this case, Price
isn’t really exposed as an entity in our API but that doesn’t mean we can’t connect it!
In this case, we just want to connect the specific Product.price
field. We won’t need the price(id: ID!): Price
entity connector we previously had. We can use the @connect
on the Product.price
field and instead of using the $args
to provide the product id, we can use $this
(which means we want to use this types id):
type Product {
id: ID!
name: String
description: String
image: String
price: Price
@connect(
source: "products"
http: { GET: "/products/{$this.id}/price" }
selection: """
id
unit
amount: unit_amount
"""
)
}
type Price {
id: ID!
unit: String
amount: Float
}
The key here is that since we already spread the Product.price
with the price_id
coming from the product entity we defined, now an incoming request that selects the pricing fields will automatically be orchestrated for you in the query plan generated by the Apollo Router:
![](https://wp.apollographql.com/wp-content/uploads/2025/02/Screenshot-2025-02-04-at-6.58.19-PM-1024x576.png)
You can identify this orchestration pattern for any URL structure that follows:
/foo/:foo_id
/foo/:foo_id/bar
It really is that simple to make connections where a simple GraphQL operation can be translated to a series of downstream REST calls – all without writing any additional code in something like a BFF!
@connect
directives
Using multiple Another pattern that you can utilize is placing multiple @connect
directives on a single field. Each @connect
can satisfy a portion of the type being returned. In our previous example, we can use a similar @connect
directive on the root product
field. The main difference, is now we can have an @connect
directive that provides the price
fields information from our second API endpoint:
type Query {
product(id:ID!): Product
@connect(
source: "products"
http: { GET: "/products/{$args.id}" }
selection: """
id
name
description
image
"""
entity: true
)
@connect(
source: "products"
http: { GET: "/products/{$args.id}/price" }
selection: """
price: {
id
unit
amount: unit_amount
}
"""
)
}
Now each connector will be only be used based on the fields selected by the incoming operation. This means that the /products/:product_id/price
endpoint will be called only if the incoming operation selects the price field (i.e. query { product(id:"1") { price { amount } } }
). This is just another option in how you can design out your connections!
Wrap up
In this post, we discussed how you can start identifying and exposing entities already defined in your REST APIs as connectors. Then we covered how we can follow a best practice of expanding “entityId” fields in our API responses into objects that enable us to connect other entities when you see this url pattern:
/foo/:foo_id
– where the API response containsbar_id
/bar/:bar_id
We also covered how to connect an entity to other endpoints where the url pattern relies on the underlying entity id:
/foo/:foo_id
/foo/:foo_id/bar
Last, we covered how you can use multiple @connect
directives as an example with the previous pattern:
type Query {
foo(id:ID!): Foo
@connect(
source: "foo"
http: { GET: "/foo/{$args.id}" }
selection: """
id
...
"""
entity: true
)
@connect(
source: "foo"
http: { GET: "/foo/{$args.id}/bar" }
selection: """
bar: {
id
...
}
"""
)
}
This is just the beginning and we’ll be sharing more fun tips on how you can speed up your development and orchestrate your APIs at lightning fast pace. You can try out connectors today by getting started with GraphOS for free. Send us your feedback and ask questions in our community forums. We can’t wait to see what you create!