You can stitch together GraphQL schemas from multiple services into a single, unified API gateway.

Let’s see this in action. Imagine you have two microservices: userService and productService.

userService schema:

type User {
  id: ID!
  username: String!
  email: String
}

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

productService schema:

type Product {
  id: ID!
  name: String!
  price: Float!
  ownerId: ID!
}

type Query {
  product(id: ID!): Product
  productsByOwner(ownerId: ID!): [Product!]!
}

Now, we want a single gateway schema that combines these. A common way to do this is using Apollo Federation.

The federated userService schema would look like this:

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

extend type Query {
  user(id: ID!): User @key(fields: "id")
}

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

Notice the @key directive. This marks User as a type that can be extended by other services, and id is the field that uniquely identifies it. The extend type Query is not strictly necessary for federation itself but is often used to expose the entry point for fetching the federated type.

The federated productService schema:

type Query {
  product(id: ID!): Product
  productsByOwner(ownerId: ID!): [Product!]!
}

extend type User @key(fields: "id") {
  products: [Product!]! @requires(fields: "id")
}

type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
  ownerId: ID!
}

Here, productService extends the User type from userService by adding a products field. The @requires(fields: "id") tells the gateway that to resolve products, it needs the id of the User.

The gateway (or "composition" layer) then combines these into a single schema. A client can now query for a user and their products in one go:

query GetUserAndProducts($userId: ID!) {
  user(id: $userId) {
    id
    username
    products {
      id
      name
      price
    }
  }
}

When this query hits the gateway, Apollo Federation (or a similar tool) orchestrates the calls. It first queries userService for user(id: $userId). Once it gets the user object, it sees that the products field on User is defined by productService and requires the id. It then calls productService with productsByOwner(ownerId: $userId) to fetch the related products. Finally, it merges the results and returns them to the client.

The actual "composition" of these schemas happens at the gateway. You’d typically have a service that acts as the entry point, loads the schemas from your federated services (often via their introspection endpoints), and composes them into a single supergraph schema.

A key benefit is that each service is only responsible for its own domain. userService doesn’t need to know about products, and productService doesn’t need to know about user details beyond what’s needed for relationships. The gateway handles the cross-service data fetching.

The most surprising thing about schema federation is that the gateway doesn’t just blindly merge types. When a query asks for a field defined in a different service (like products on User in our example), the gateway looks at the @key directive on the parent type (User) and the @requires directive on the requested field. It then knows which service owns that field and what information it needs from the parent to resolve it. This allows for complex, distributed data fetching without the client needing to know about the underlying service boundaries.

The next hurdle you’ll typically encounter is handling mutations across federated services, especially when a mutation in one service needs to affect data owned by another.

Want structured learning?

Take the full Graphql-tools course →