Federation plugin
Turn a Pothos schema into an Apollo Federation 2 subgraph, with entities, external-type extensions, and composable subgraph output.
Federation lets you split one graph across services. The federation plugin turns a Pothos schema into an Apollo Federation 2 subgraph: you mark object types as entities, describe how each is loaded by reference, and emit a subgraph schema the gateway can compose. This page covers the Pothos API; for what the federation terms mean, see the Apollo docs.
Examples use an Ultimate League graph split across subgraphs: a teams service that owns Team, a roster service that extends Player, and a ratings service that references both.
Install
The plugin needs the directives plugin and @apollo/subgraph alongside it:
npm install --save @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraphAdd @apollo/server too if you serve the subgraph with Apollo; it is not required if you run a different server:
npm install --save @apollo/serverSetup
List the directives plugin before the federation plugin. If you use resolver-wrapping plugins like scope-auth, the federation plugin should come after them so it sees the final resolvers:
import SchemaBuilder from '@pothos/core';
import DirectivesPlugin from '@pothos/plugin-directives';
import FederationPlugin from '@pothos/plugin-federation';
const builder = new SchemaBuilder({
plugins: [DirectivesPlugin, FederationPlugin],
});Defining entities
An entity is an object type another service can reference or extend. Defining one is two steps: declare the object type as you normally would with objectRef, then promote it with builder.asEntity by giving it a key and a resolveReference.
const TeamType = builder.objectRef<ITeam>('Team').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
city: t.exposeString('city'),
}),
});
builder.asEntity(TeamType, {
key: builder.selection<{ id: string }>('id'),
resolveReference: ({ id }) => teams.find((team) => team.id === id),
});Keys are built with builder.selection, which must be called with a generic argument spelling out the types of every field in the key. key also accepts an array when an entity has more than one key. resolveReference receives an object of the key's shape and returns the backing model; the gateway calls it whenever another service references this Team by key.
Type the key with the scalar shapes your server produces, not the shapes your resolvers return. Apollo Server serializes every ID to a string, so a key over an id field is selection<{ id: string }>('id') even if your data holds id as a number.
Extending external entities
To add fields to an entity owned by another service, call builder.externalRef and then implement the returned ref. externalRef takes the entity name, a key selection, and a resolver that loads the local data for a given key. The resolver's return type becomes the backing model (the parent your added fields resolve against), and the key describes which fields the gateway selects from the owning service to build that parent.
const PlayerRef = builder.externalRef(
'Player',
builder.selection<{ id: string }>('id'),
(entity) => {
const stats = playerStats.find(({ id }) => id === entity.id);
// extend the referenced key with data this service owns
return stats && { ...entity, ...stats };
},
);
PlayerRef.implement({
// external fields let `requires`/`provides` reference data owned elsewhere
externalFields: (t) => ({
salary: t.int(),
seasons: t.int(),
}),
fields: (t) => ({
id: t.exposeID('id'),
goals: t.exposeInt('goals'),
contractValue: t.int({
// pull external fields into this resolver with a `requires` directive;
// they arrive as the first resolver arg
requires: builder.selection<{ salary?: number; seasons?: number }>('salary seasons'),
resolve: (data) => (data.salary ?? 0) * (data.seasons ?? 0),
}),
}),
});Fields listed under externalFields are declared as @external; they exist on the entity elsewhere and are only referenced here, by requires (above) or provides (below). A field's requires selection then makes those values available as the first resolver argument.
To mark a key's external field as non-resolvable (resolvable: false), wrap the selection with builder.keyDirective:
const PlayerRef = builder.externalRef(
'Player',
builder.keyDirective(builder.selection<{ id: string }>('id'), false),
);Adding a provides directive
@provides lets a field promise that its result already carries certain fields of a referenced entity, so the gateway skips a round trip. Implement the referenced type as an external ref that lists the provided field under externalFields, then set the field's type to Ref.provides<Shape>('...') instead of the bare ref. The generic works like builder.selection, and using .provides both emits the annotation and ensures the resolved value includes the provided data.
const TeamType = builder.externalRef('Team', builder.selection<{ id: string }>('id')).implement({
externalFields: (t) => ({
// the field this service will provide
name: t.string(),
}),
fields: (t) => ({
id: t.exposeID('id'),
}),
});
const RatingType = builder.objectRef<IRating>('Rating');
RatingType.implement({
fields: (t) => ({
id: t.exposeID('id'),
score: t.exposeInt('score'),
team: t.field({
// TeamType.provides<...> annotates the field and requires the resolved
// value to include the provided `name`
type: TeamType.provides<{ name: string }>('name'),
resolve: (rating) => ({
id: rating.teamID,
name: teamNames.find((team) => team.id === rating.teamID)!.name,
}),
}),
}),
});The provided field must be one of the external ref's externalFields; you can only provide what the entity declares.
Field and type directives
Several federation directives are plain options on a field or type definition rather than separate API calls:
t.field({
type: 'String',
shareable: true,
tag: ['public'],
inaccessible: true,
override: { from: 'roster' },
});| Option | Directive | Applies to |
|---|---|---|
shareable | @shareable | fields, object types |
tag | @tag | fields, object types, input fields |
inaccessible | @inaccessible | fields, types, input fields, enum values |
override | @override; { from, label? } names the service being overridden | fields |
authenticated | @authenticated | fields, object/interface/scalar/enum types |
requiresScopes | @requiresScopes | same as authenticated |
policy | @policy | same as authenticated |
cost | @cost | fields, object/scalar/enum types, input fields, enum values |
listSize | @listSize with { assumedSize?, slicingArguments?, sizedFields?, requireOneSlicingArgument? } | fields |
requiresScopes and policy take a nested array (string[][]) of scopes/policies. Type them by setting the FederationScopes and FederationPolicies generics on the builder. See the official Federation docs for each directive's semantics.
Interface entities and @interfaceObject
Federation 2.3 added interface entities. Pass an interface ref to asEntity to give an interface a key:
const Media = builder.interfaceRef<{ id: string }>('Media').implement({
fields: (t) => ({
id: t.exposeID('id'),
// ...shared fields
}),
});
builder.asEntity(Media, {
key: builder.selection<{ id: string }>('id'),
resolveReference: ({ id }) => loadMediaById(id),
});To add fields to every implementor of an interface owned by another subgraph, define an @interfaceObject: declare it as an object type (not an interface) and set interfaceObject: true on asEntity.
const Media = builder.objectRef<{ id: string }>('Media').implement({
fields: (t) => ({
id: t.exposeID('id'),
// new fields here apply to every implementor of Media
}),
});
builder.asEntity(Media, {
interfaceObject: true,
key: builder.selection<{ id: string }>('id'),
resolveReference: (ref) => ref,
});Building the subgraph schema
Call builder.toSubGraphSchema instead of toSchema; it adds the federation-specific _entities/_service queries and the @link directive. linkUrl defaults to https://specs.apollo.dev/federation/v2.6; federationDirectives defaults to the set of directives your schema actually uses.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const schema = builder.toSubGraphSchema({
// override the federation version if you need an older one
linkUrl: 'https://specs.apollo.dev/federation/v2.3',
// usually left to default — the plugin infers this from your schema
federationDirectives: ['@key', '@external', '@requires', '@provides'],
});
const server = new ApolloServer({ schema });
startStandaloneServer(server, { listen: { port: 4000 } })
.then(({ url }) => {
console.log(`🚀 Subgraph ready at ${url}`);
})
.catch((error) => {
throw error;
});For a runnable example that composes several Pothos subgraphs into one supergraph, see the federation test example.
If you print the schema to a string (for Managed Federation or manual composition with rover), use printSubgraphSchema from @apollo/subgraph. The default graphql-js printer drops the directives federation relies on, so a plainly-printed schema will not compose.
composeDirective
To preserve a custom directive through composition, pass composeDirectives to toSubGraphSchema. It needs a matching @link (via schemaDirectives) pointing at your directive's spec, plus a real GraphQLDirective implementation:
import { DirectiveLocation, GraphQLDirective } from 'graphql';
export const schema = builder.toSubGraphSchema({
// emits @composeDirective(name: "@custom")
composeDirectives: ['@custom'],
// @composeDirective requires an @link to your directive's URL
schemaDirectives: {
link: { url: 'https://myspecs.dev/myCustomDirective/v1.0', import: ['@custom'] },
},
// and an actual implementation of the directive
directives: [
new GraphQLDirective({
name: 'custom',
locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
}),
],
});