Federation to Stitching SDL
This example demonstrates the integration of Apollo Federation services into a stitched schema.
As you get the hang of schema stitching, you may find that
Federation services are fairly complex for what
they do. The buildFederatedSchema
method from the @apollo/federation
package creates a nuanced
GraphQL resource that does not guarentee itself to be independently consistent, but plugs seamlessly
into a greater automation package. By comparison, stitching encourages services to be independently
valid and self-contained GraphQL resources, which makes them quite primitive and durable. While
Federation automates service bindings at the cost
of tightly-coupled complexity, stitching embraces loosely-coupled bindings at the cost of manual
setup. The merits of each strategy are likely to be a deciding factor for developers selecting a
platform. Stitching is a library used to build a framework like Federation.
Stitching is less opinionated than Federation, and is made considerably simpler without the
complexity added by buildFederatedSchema
. However, when integrating with existing servers or in
the process of a migration, nothing says you can’t incorporate your existing federation resources
into a stitched gateway going through the
federation _entities
query
– which is fundamentally just a GraphQL service.
This example demonstrates:
- Integrating Apollo Federation services into a stitched schema.
- Fetching and parsing Federation SDLs.
Sandbox
⬇️ Click ☰ to see the files
You can also see the project on GitHub here.
The following services are available for interactive queries:
- Stitched gateway: listening on 4000/graphql
- Products subservice: listening on 4001/graphql
- Reviews subservice: listening on 4002/graphql
- Users subservice: listening on 4003/graphql
This example is based on the Federation intro example.
Summary
Ever wonder what Federation is doing under the hood? Visit the
products service and check out some User
objects:
query {
_entities(
representations: [
{ __typename: "User", id: "1" }
{ __typename: "User", id: "2" }
{ __typename: "User", id: "3" }
]
) {
... on User {
id
recentPurchases {
upc
name
price
}
}
}
}
A federation service automatically configures an _entities
query that recieves typed keys (i.e.:
objects with a __typename
), and returns abstract _Entity
objects that may assume the shape of
any type in the service. Apollo Gateway then
automates the exchange of typed keys for typed results, all going through the dedicated _entities
protocol in each subservice. Stitching can also integrate with this _entities
query by sending it
properly formatted keys.
Now go to the gateway and check out the stitched results:
query {
user(id: "1") {
username
recentPurchases {
upc
name
}
reviews {
body
author {
id
username
}
product {
upc
name
acceptsNewReviews
}
}
}
}
The stitched gateway has loaded all federation SDLs, converted them into stitching SDLs, and then
integrates them like any other GraphQL service with types merged through their _entities
query.
Adapting Federation services
Federation and Stitching use fundamentally similar patterns to combine underlying subservices (in fact, both tools have shared origins in Apollo Stitching). However, their specific implementations have an important differentiator:
- Apollo Federation uses a centralized approach, where all types have a single “origin” service (i.e.: where the unextended type definition is). Querying for a type builds from its origin service.
- Stitching uses a decentralized approach, where any service may equally originate any type. Regardless of where a typed object is first represented, that original object is filled in with missing details from other services.
How each system handles origins informs how a federation service gets translated into a stitched subschema:
- All types with a
@key
directive become merged types; the key fields go intoselectionSet
. - All fields with a
@requires
directive are made into computed fields. Computed fields are slightly more robust than their federation counterparts because they may resolve dependencies from any number of services. - All fields with an
@external
directive are removed unless they are part of the@key
. Stitching expects schemas to only publish fields that they actually have data for. This is considerably simpler than the federation approach where services may be responsible for data they don’t have. - By eliminating the indirection of
@external
fields, the@provides
directive is no longer necessary. The Stitching query planner can automate the optimial selection of as many fields as possible from as few services as possible.
SDL integration
The simplest way to make the above adaptions is to translate a Federation SDL string into a
Stitching SDL string, which can be done using the federationToStitchingSDL
utility function from
@graphql-tools/stitching-directives
package. A federation service’s SDL can be obtained through its _service
API:
query {
_service {
sdl
}
}
Once fetched, it can be translated into a Stitching SDL and then built into a stitched schema:
import { buildSchema } from 'graphql'
import { buildHTTPExecutor } from '@graphql-tools/executor-http'
import { federationToStitchingSDL, stitchingDirectives } from '@graphql-tools/stitching-directives'
const stitchingConfig = stitchingDirectives()
const executor = buildHTTPExecutor({ endpoint: 'http://localhost:4001/graphql' })
const federationSDL = await executor({
document: parse(/* GraphQL */ `
{
_service {
sdl
}
}
`)
})
const stitchingSDL = federationToStitchingSDL(federationSDL, stitchingConfig)
const gatewaySchema = stitchSchemas({
subschemaConfigTransforms: [stitchingConfig.stitchingDirectivesTransformer],
subschemas: [
{
schema: buildSchema(stitchingSDL),
executor
}
]
})
Static config
Written as static subservice configuration, a federation service merges types within a stitched gateway using the following:
import { pick } from 'lodash'
const gatewaySchema = stitchSchemas({
subschemas: [
{
schema: buildSchema(stitchingSDL),
merge: {
Product: {
selectionSet: '{ id }',
fields: {
shippingEstimate: { selectionSet: '{ price weight }', computed: true }
},
fieldName: '_entities',
key: originObj => ({
__typename: 'Product',
...pick(originObj, ['id', 'price', 'weight'])
}),
argsFromKeys: representations => ({ representations })
}
}
}
]
})