bookworm-smart-assistant/skills/graphql-architect/references/federation.md

9.4 KiB

Apollo Federation

Subgraph Setup

// users-subgraph/schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@shareable"])

type User @key(fields: "id") {
  id: ID!
  email: String!
  username: String!
  createdAt: DateTime!
}

type Query {
  user(id: ID!): User
  users: [User!]!
}

// users-subgraph/resolvers.ts
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { readFileSync } from 'fs';

const typeDefs = readFileSync('./schema.graphql', 'utf8');

const resolvers = {
  User: {
    __resolveReference: async (
      reference: { id: string },
      context: Context
    ): Promise<User> => {
      return context.dataSources.users.findById(reference.id);
    },
  },

  Query: {
    user: async (parent, args: { id: string }, context: Context) => {
      return context.dataSources.users.findById(args.id);
    },
    users: async (parent, args, context: Context) => {
      return context.dataSources.users.findAll();
    },
  },
};

const server = new ApolloServer({
  schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});

Entity Keys and References

# products-subgraph/schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: [
    "@key",
    "@shareable",
    "@interfaceObject"
  ])

# Single key field
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
  sku: String! @shareable
}

# Composite key
type Variant @key(fields: "productId sku") {
  productId: ID!
  sku: String!
  size: String!
  color: String!
}

# Multiple keys (different ways to identify)
type Review @key(fields: "id") @key(fields: "productId authorId") {
  id: ID!
  productId: ID!
  authorId: ID!
  rating: Int!
  content: String!
}

Extending Types Across Subgraphs

# users-subgraph: owns User
type User @key(fields: "id") {
  id: ID!
  email: String!
  username: String!
}

# posts-subgraph: extends User with posts
extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post!]!
}

type Post @key(fields: "id") {
  id: ID!
  title: String!
  content: String!
  authorId: ID!
  author: User!
}
// posts-subgraph/resolvers.ts
const resolvers = {
  User: {
    // Reference resolver: fetch User stub by id
    __resolveReference: async (
      reference: { id: string },
      context: Context
    ) => {
      return { id: reference.id };
    },

    // Field resolver: resolve posts for User
    posts: async (user: { id: string }, args, context: Context) => {
      return context.dataSources.posts.findByAuthor(user.id);
    },
  },

  Post: {
    // Resolve author as User entity reference
    author: (post: Post) => {
      return { __typename: 'User', id: post.authorId };
    },
  },
};

Federation Directives

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: [
    "@key",
    "@requires",
    "@provides",
    "@external",
    "@shareable",
    "@override",
    "@inaccessible",
    "@tag"
  ])

# @key: Define entity with primary key
type Product @key(fields: "id") {
  id: ID!
  name: String!
}

# @external: Field defined in another subgraph
extend type User @key(fields: "id") {
  id: ID! @external
  email: String! @external
  isVerified: Boolean! @external
}

# @requires: Field needs external data
extend type User @key(fields: "id") {
  id: ID! @external
  email: String! @external
  isVerified: Boolean! @external
  # Can only compute if we have email and isVerified
  canPost: Boolean! @requires(fields: "email isVerified")
}

# @provides: Optimization hint
type Post @key(fields: "id") {
  id: ID!
  author: User! @provides(fields: "username")
}

# @shareable: Field can be resolved by multiple subgraphs
type Product @key(fields: "id") {
  id: ID!
  sku: String! @shareable
  name: String!
}

# @override: Migration between subgraphs
type Product @key(fields: "id") {
  id: ID!
  # Override from legacy-subgraph
  price: Float! @override(from: "legacy-subgraph")
}

# @inaccessible: Hide from supergraph
type User @key(fields: "id") {
  id: ID!
  email: String!
  internalId: String! @inaccessible
}

# @tag: Organize schema
type Query {
  products: [Product!]! @tag(name: "public")
  adminUsers: [User!]! @tag(name: "admin")
}

Gateway Configuration

// gateway/server.ts
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/graphql' },
      { name: 'posts', url: 'http://localhost:4002/graphql' },
      { name: 'products', url: 'http://localhost:4003/graphql' },
    ],
    // Poll for schema updates
    pollIntervalInMs: 10000,
  }),

  // Error handling
  serviceHealthCheck: true,

  // Query planning debug
  debug: process.env.NODE_ENV === 'development',
});

const server = new ApolloServer({
  gateway,

  // Context propagation to subgraphs
  context: async ({ req }) => {
    const token = req.headers.authorization || '';
    return { token };
  },
});

await server.listen(4000);
console.log('Gateway ready at http://localhost:4000');

Managed Federation (Apollo Studio)

// gateway/server.ts with managed federation
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';

const gateway = new ApolloGateway({
  // No subgraph URLs needed - fetched from Apollo Studio
  // Schema composition happens in Apollo Studio
  async supergraphSdl({ update }) {
    // Fetch from Apollo Uplink
    const supergraphSdl = await fetchSupergraphSdl();
    return {
      supergraphSdl,
      cleanup: async () => {},
    };
  },
});

// Subgraph reporting to Apollo Studio
import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace';

const subgraphServer = new ApolloServer({
  schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
  plugins: [
    ApolloServerPluginInlineTrace(),
  ],
});

Value Types vs Entities

# Value type: no @key, resolved entirely by one subgraph
type Address {
  street: String!
  city: String!
  country: String!
  postalCode: String!
}

# Entity: has @key, can be extended by other subgraphs
type User @key(fields: "id") {
  id: ID!
  email: String!
  # Value type embedded in entity
  address: Address
}

# Another subgraph can extend User but not Address
extend type User @key(fields: "id") {
  id: ID! @external
  orders: [Order!]!
}

Interface Objects

# accounts-subgraph
type User implements Account @key(fields: "id") {
  id: ID!
  email: String!
  role: String!
}

type AdminUser implements Account @key(fields: "id") {
  id: ID!
  email: String!
  role: String!
  permissions: [String!]!
}

interface Account {
  id: ID!
  email: String!
  role: String!
}

# orders-subgraph (doesn't know about User/AdminUser)
extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@interfaceObject"])

type Order @key(fields: "id") {
  id: ID!
  account: Account!
}

# Use @interfaceObject to reference Account without knowing implementations
type Account @key(fields: "id") @interfaceObject {
  id: ID!
}

Query Planning Optimization

# Inefficient: requires multiple roundtrips
type Query {
  user(id: ID!): User
}

type User @key(fields: "id") {
  id: ID!
  posts: [Post!]!
}

extend type Post @key(fields: "id") {
  id: ID! @external
  author: User!
}

# Better: provide data to avoid extra fetch
type Post @key(fields: "id") {
  id: ID!
  authorId: ID!
  # Optimization: provide username directly
  author: User! @provides(fields: "username")
}

# Gateway can fulfill some User fields from Post subgraph
# without fetching from User subgraph

Error Handling in Federation

const resolvers = {
  User: {
    __resolveReference: async (
      reference: { id: string },
      context: Context
    ) => {
      try {
        const user = await context.dataSources.users.findById(reference.id);
        if (!user) {
          // Return null for missing entity (soft error)
          return null;
        }
        return user;
      } catch (error) {
        // Hard error propagates to client
        throw new GraphQLError('Failed to resolve user', {
          extensions: {
            code: 'USER_RESOLUTION_FAILED',
            userId: reference.id,
          },
        });
      }
    },
  },
};

Federation Best Practices

  1. Entity Design: Use @key for types that need to be extended
  2. Subgraph Boundaries: Align with team/service boundaries
  3. Shared Types: Use @shareable for truly shared fields
  4. Migration: Use @override for gradual subgraph migration
  5. Performance: Use @provides to optimize query planning
  6. Value Types: Use plain types for embedded data
  7. Composition: Test schema composition in CI/CD
  8. Versioning: Use managed federation for safe deployments
  9. Monitoring: Track query planning and resolver performance
  10. Documentation: Document entity ownership and extension patterns