StackPractices
advanced Por Mathias Paulenko

Guía Completa de GraphQL Federation

Construye APIs GraphQL unificadas con Apollo Federation. Cubre subgraphs, supergraph, resolución de entidades y despliegue de gateway.

Introducción

Construir un único schema GraphQL para toda la empresa se convierte rápidamente en un cuello de botella. Los equipos se bloquean mutuamente con cambios de schema, los despliegues quedan acoplados y el grafo monolítico se vuelve frágil. GraphQL Federation resuelve esto dividiendo el schema en subgraphs que cada equipo posee y componiéndolos de vuelta en un supergraph. Esta guía muestra cómo configurar subgraphs, componer el supergraph, resolver entidades y desplegar un gateway con Apollo Federation.

Vengo trabajando con GraphQL desde 2018, y vi el patrón monolítico fallar en tres empresas distintas. La historia es siempre la misma: un equipo necesita añadir un campo, otro está en medio de una migración, y la reunión de review del schema se convierte en un debate de dos horas. Federation no arregla problemas organizacionales, pero le da a los equipos límites de ownership claros. Cada equipo posee su subgraph, lo despliega de forma independiente, y el gateway los compone en una sola superficie de API para los clientes.

Si venís de REST, el modelo mental es distinto. En lugar de construir una API por servicio y dejar que los clientes orquesten las llamadas, componés un grafo unificado. Los clientes consultan un endpoint y el gateway averigua a qué subgraphs llamar. Para una comparación más profunda, mirá GraphQL vs REST: Guía Completa. Si ya corrés microservicios, federation encaja naturalmente arriba. Mirá la Guía Completa de Comunicación entre Microservicios para contexto sobre cómo interactúan los servicios.

Esta guía asume que conocés los basics de GraphQL: schemas, resolvers y queries. No necesitás experiencia previa con Apollo, pero familiaridad con Node.js y Python ayuda porque los ejemplos usan ambos. Al final, vas a tener un grafo federado funcional con tres subgraphs, un gateway y un supergraph compuesto con Rover. Cubriremos la arquitectura, la configuración de subgraphs en Node.js y Python, la configuración del gateway, la composición del supergraph, la resolución de entidades con todas las directivas clave, los patrones de consulta, las mejores prácticas y los errores comunes. Si buscás guía específica de producción sobre despliegue, monitoreo y federación administrada, mirá la Guía Completa de GraphQL Federation en Producción.

Arquitectura de Federation

flowchart diagram: Cliente

Un subgraph es un servicio GraphQL poseído por un equipo que define parte del schema. El supergraph es el schema compuesto a partir de todos los subgraphs. El gateway es el punto de entrada que enruta cada parte de una query al subgraph correspondiente. Una entidad es un type compartido con un key field que múltiples subgraphs pueden referenciar y extender.

El insight clave es que los subgraphs no se llaman entre sí directamente. El gateway construye un query plan, llama a los subgraphs en el orden correcto y une los resultados. Los subgraphs solo necesitan saber sobre las entidades que referencian, no sobre el grafo completo. Esto mantiene el acoplamiento bajo y deja a los equipos trabajar independientemente.

Hay dos modelos de composición: federación administrada (Apollo Studio hostea el schema del supergraph y el gateway lo fetcha) y federación no administrada (componés el supergraph localmente y el gateway lo carga desde un archivo o URL). La federación administrada es mejor para producción porque rastrea cambios de schema, valida la composición en CI y te deja hacer rollback. La no administrada está bien para desarrollo y despliegues chicos. Uso federación administrada en producción y no administrada para desarrollo local.

Configuración de Subgraphs

Cada subgraph es un servicio GraphQL standalone. Define sus propios types, queries y mutations. Las únicas adiciones específicas de federation son las directivas (@key, @external, @extends, @requires, @provides, @shareable) que le dicen al motor de composición cómo se relacionan los types entre subgraphs. La especificación de Apollo Federation define estas directivas. Al principio me confundieron, pero son solo anotaciones en tu schema. No cambiás cómo funcionan los resolvers; añadís metadatos que el gateway lee durante la composición.

Subgraph de Users (Node.js)

El subgraph de Users posee el type User. Lo marca como entidad con @key(fields: "id"), lo que significa que otros subgraphs pueden referenciar un usuario por su id sin necesidad de saber cómo resolverlo. El subgraph también extiende Order y Product para añadir relaciones desde la perspectiva del usuario. En la práctica, esto significa que el equipo de Users controla cómo se ve un User, y otros equipos pueden stitchear sus datos arriba.

const { buildSubgraphSchema } = require("@apollo/subgraph");
const { gql, ApolloServer } = require("apollo-server");

const typeDefs = gql`
  type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
    orders: [Order!]!
  }

  extend type Order @key(fields: "id") {
    id: ID! @external
    user: User! @provides(fields: "name")
  }

  extend type Product @key(fields: "id") {
    id: ID! @external
  }

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

const resolvers = {
  User: {
    orders(user) {
      return fetch(`http://orders-service/orders?userId=${user.id}`)
        .then((res) => res.json());
    },
  },
  Query: {
    user: (_, { id }) => fetch(`http://users-service/users/${id}`).then((res) => res.json()),
    users: () => fetch("http://users-service/users").then((res) => res.json()),
  },
};

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

server.listen({ port: 4001 }).then(({ url }) => {
  console.log(`Users subgraph ready at ${url}`);
});

Subgraph de Orders (Node.js)

El subgraph de Orders posee el type Order y sus campos. Referencia a User como entidad devolviendo un objeto de referencia { __typename: "User", id: order.userId } en lugar de fetchear el usuario completo. El gateway resuelve los campos del usuario llamando al subgraph de Users con esa entity key. Este es el núcleo de federation: los subgraphs devuelven referencias de entidades, y el gateway las sigue a través de los límites de los subgraphs.

El resolver Order.user devuelve un objeto de referencia, no un usuario completo. Esto es intencional. El gateway va a llamar al subgraph de Users para completar los campos del usuario. Si el cliente solo pide el id y total de la orden, el gateway nunca llama al subgraph de Users. Esta resolución lazy es lo que hace federation eficiente.

const { buildSubgraphSchema } = require("@apollo/subgraph");
const { gql, ApolloServer } = require("apollo-server");

const typeDefs = gql`
  type Order @key(fields: "id") {
    id: ID!
    total: Float!
    status: String!
    userId: ID!
    user: User!
    items: [OrderItem!]!
  }

  type OrderItem {
    productId: ID!
    quantity: Int!
    price: Float!
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    orders: [Order!]! @external
  }

  extend type Product @key(fields: "id") {
    id: ID! @external
    orders: [OrderItem!]!
  }

  type Query {
    order(id: ID!): Order
    orders: [Order!]!
  }

  type Mutation {
    createOrder(userId: ID!, items: [OrderItemInput!]!): Order!
  }

  input OrderItemInput {
    productId: ID!
    quantity: Int!
  }
`;

const resolvers = {
  Order: {
    user(order) {
      return { __typename: "User", id: order.userId };
    },
    items(order) {
      return order.items;
    },
  },
  Product: {
    orders(product) {
      return fetch(`http://orders-service/orders/items?productId=${product.id}`)
        .then((res) => res.json());
    },
  },
  Query: {
    order: (_, { id }) => fetch(`http://orders-service/orders/${id}`).then((res) => res.json()),
    orders: () => fetch("http://orders-service/orders").then((res) => res.json()),
  },
  Mutation: {
    createOrder: (_, { userId, items }) => {
      return fetch("http://orders-service/orders", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ userId, items }),
      }).then((res) => res.json());
    },
  },
};

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

server.listen({ port: 4002 }).then(({ url }) => {
  console.log(`Orders subgraph ready at ${url}`);
});

Subgraph de Products (Python)

El subgraph de Products usa Python con Ariadne, que soporta federation a través de make_federated_schema. El resolver __resolve_reference es lo que hace funcionar la entidad Product a través de los límites de los subgraphs. Cuando el gateway necesita resolver una referencia de producto desde el subgraph de Orders, llama a este resolver con la entity key y el subgraph de Products devuelve el producto completo.

Elegí Ariadne para este ejemplo porque es la librería de federation en Python que mejor conozco. Si usás Strawberry, el enfoque es similar: definís una directiva @key en el type e implementás un método resolve_reference de clase. El protocolo de federation es language-agnostic, así que cualquier servidor GraphQL que lo use funciona.

from ariadne import QueryType, make_federated_schema, ObjectType
from ariadne.asgi import GraphQL
import httpx

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

    type Query {
        product(id: ID!): Product
        products: [Product!]!
    }
"""

query = QueryType()
product_obj = ObjectType("Product")

@query.field("product")
async def resolve_product(_, info, id):
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"http://products-service/products/{id}")
        return resp.json()

@query.field("products")
async def resolve_products(_, info):
    async with httpx.AsyncClient() as client:
        resp = await client.get("http://products-service/products")
        return resp.json()

@product_obj.field("__resolve_reference")
async def resolve_product_reference(reference, info):
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"http://products-service/products/{reference['id']}")
        return resp.json()

schema = make_federated_schema(type_defs, [query, product_obj])
app = GraphQL(schema, debug=True)

Configuración del Gateway

El gateway es el único punto de entrada para los clientes. Recibe queries, construye un query plan, llama a los subgraphs y une los resultados. La configuración serviceList le dice al gateway dónde vive cada subgraph. En producción, usarías federación administrada en lugar de una service list hardcodeada para que el gateway pueda pickear cambios de schema sin redesplegar.

const { ApolloGateway } = require("@apollo/gateway");
const { ApolloServer } = require("apollo-server");

const gateway = new ApolloGateway({
  serviceList: [
    { name: "users", url: "http://localhost:4001/graphql" },
    { name: "orders", url: "http://localhost:4002/graphql" },
    { name: "products", url: "http://localhost:4003/graphql" },
  ],
  debug: true,
});

const server = new ApolloServer({
  gateway,
  subscriptions: false,
});

server.listen({ port: 4000 }).then(({ url }) => {
  console.log(`Gateway ready at ${url}`);
});

El gateway maneja el query planning automáticamente. Cuando un cliente pide un usuario y sus órdenes, el gateway primero llama al subgraph de Users, extrae el id del usuario, después llama al subgraph de Orders con ese id como entity key. Une los resultados y devuelve una sola response. El cliente nunca ve los límites de los subgraphs.

Para producción, considerá el Apollo Router, un gateway basado en Rust que es más rápido que el Apollo Gateway de Node.js. Soporta el mismo protocolo de federation pero maneja mayor throughput con menor latencia. Usé ambos: el gateway de Node.js está bien para desarrollo y despliegues chicos, y el Router es mejor cuando necesitás manejar miles de queries por segundo.

Composición del Supergraph

Usá el Rover CLI para componer el schema del supergraph a partir de los subgraphs en ejecución. La composición toma todos los schemas de los subgraphs, los valida contra la especificación de federation y produce un solo schema de supergraph que el gateway usa para planear queries.

# Instalar Rover
brew install apollo-tooling/tap/rover

# Componer supergraph desde los schemas de los subgraphs
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# supergraph.yaml
federation_version: =2.8.0
subgraphs:
  users:
    routing_url: http://localhost:4001/graphql
    schema:
      subgraph_url: http://localhost:4001/graphql
  orders:
    routing_url: http://localhost:4002/graphql
    schema:
      subgraph_url: http://localhost:4002/graphql
  products:
    routing_url: http://localhost:4003/graphql
    schema:
      subgraph_url: http://localhost:4003/graphql

La composición puede fallar si dos subgraphs definen el mismo campo sin @shareable, o si un subgraph usa @requires sobre un campo que no está marcado con @external. Estos errores aparecen en CI, que es donde los querés. Corro rover supergraph compose en cada pipeline de CI para que los conflictos de schema nunca lleguen a producción. La documentación de Apollo sobre composición cubre la lista completa de reglas de composición y mensajes de error.

En federación administrada, publicás los schemas de los subgraphs a Apollo Studio con rover subgraph publish en lugar de componer localmente. Studio compone el supergraph, lo valida y lo pushea al gateway. Lo recomiendo para cualquier setup de producción porque te da un historial de schema, validación de composición y rollback con un click. La primera vez que una composición rompe en producción a las 2am, vas a estar contento de tener ese botón de rollback.

Resolución de Entidades

Las entidades son el núcleo de federation. Permiten que un subgraph referencie un type poseído por otro subgraph sin duplicar su definición. Cuando el gateway necesita resolver una referencia de entidad, llama al resolver __resolveReference del subgraph que la posee con la entity key. El subgraph devuelve el objeto completo y el gateway completa los campos que el cliente pidió.

Pienso en las entidades como las join tables de federation. En un schema monolítico, resolverías las órdenes de un usuario con un solo resolver que tiene acceso a ambas fuentes de datos (users y orders). En federation, el subgraph de Users devuelve una entidad User, el subgraph de Orders la extiende con orders, y el gateway los une. Los subgraphs nunca hablan entre sí directamente.

@key: definir una entidad

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

@extends: extender una entidad de otro subgraph

extend type User @key(fields: "id") {
  id: ID! @external
  orders: [Order!]!
}

@requires: computar fields basados en campos externos

extend type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @external
  discountedPrice: Float! @requires(fields: "price")
}

@provides: indicar que un subgraph puede proveer campos de otro type

extend type Order @key(fields: "id") {
  id: ID! @external
  user: User! @provides(fields: "name")
}

@shareable: marcar un campo como resoluble por múltiples subgraphs

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

@shareable le dice al motor de composición que dos o más subgraphs pueden resolver este campo. Usalo con moderación. Si cada subgraph puede resolver cada campo, perdiste el límite de ownership que federation se supone que debe reforzar. Uso @shareable solo para campos que genuinamente necesitan venir de dos o más fuentes, como un Product.id que tanto el subgraph de Products como el de Orders necesitan devolver.

@inaccessible: ocultar un campo del API público

type User @key(fields: "id") {
  id: ID!
  internalNotes: String @inaccessible
}

@inaccessible te deja definir un campo en un subgraph sin exponerlo a los clientes. El gateway lo saca del schema público. Lo uso para campos internos que los subgraphs necesitan para cálculos de @requires pero que no deberían ser consultables por clientes.

Federation 2 vs Federation 1

Federation 2 simplificó el modelo de directivas. En Federation 1, necesitabas @extends en cada type extendido y @external en cada campo foráneo. Federation 2 hizo @extends opcional e introdujo @shareable y @inaccessible. Si estás empezando de cero, usá Federation 2. Si estás en Federation 1, la guía de migración cubre los cambios. Migré un grafo federado de v1 a v2 y el principal beneficio fue menos boilerplate en los schemas de los subgraphs.

Consultando el Grafo Federado

Esta query abarca los tres subgraphs. El gateway envía la parte de usuario al subgraph de Users, usa el id para traer las órdenes del subgraph de Orders y resuelve cada producto del subgraph de Products.

query GetUserWithOrders {
  user(id: "1") {
    id
    name
    email
    orders {
      id
      total
      status
      items {
        quantity
        product {
          name
          price
        }
      }
    }
  }
}

El gateway construye un query plan que se ve más o menos así: llama a Users para obtener el usuario, extrae el id, llama a Orders con ese id para obtener las órdenes, extrae el productId de cada item, llama a Products con esos IDs para obtener los nombres y precios de los productos. El cliente ve una sola response, como si hubiera consultado un API monolítica.

Si el subgraph de Products está caído, el gateway puede devolver datos parciales: el usuario y las órdenes llegan, pero el campo product devuelve null con una extensión de error. Esta degradación elegante es una de las mayores ventajas de federation sobre un monolito, donde el fallo de un solo resolver puede romper toda la query. Para más sobre testing de schemas federados, mirá la Guía Completa de GraphQL Testing.

Mejores Prácticas

Mantené un subgraph por equipo, así los límites de ownership coinciden con los límites del equipo. Usá @key en cualquier type que más de un subgraph necesite referenciar. Cada subgraph debería ser lo suficientemente autocontenido como para ejecutarse y probarse solo. Vi equipos intentar dividir un solo servicio en cinco subgraphs porque pensaban que más subgraphs significaba más flexibilidad. No es así. Significa más infraestructura, más errores de composición y más complejidad en los query plans. Empezá con dos o tres subgraphs y dividí solo cuando un equipo genuinamente necesita despliegue independiente.

Marcá los campos foráneos con @external en lugar de redefinirlos. Evitá extensiones circulares donde dos subgraphs se referencien mutuamente. Componé el supergraph con Rover antes de desplegar, así los conflictos de schema aparecen en CI, no en producción. Corro rover supergraph compose como paso de CI en cada pull request que toca un schema de subgraph. Si la composición falla, el PR queda bloqueado. Esto atrapa issues como campos duplicados sin @shareable antes de que lleguen a staging.

Cacheá la resolución de entidades en el gateway, porque __resolveReference se ejecuta con frecuencia. Monitoreá los query plans para entender cómo una query del cliente se convierte en múltiples llamadas a subgraphs. Si usás Apollo Studio, la federación administrada ayuda a rastrear cambios de schema y errores de composición entre entornos. El visor de query plans en Studio es invaluable para detectar patrones N+1 temprano.

Versioná los subgraphs de forma independiente; el gateway se encarga de la composición. Cuando un subgraph falla, diseñá el gateway para devolver datos parciales y extensiones de error en lugar de fallar toda la request. Configurá timeouts en cada llamada a subgraph, así un servicio lento no bloquea toda la query. Lo aprendí por las malas cuando un subgraph de Products lento hacía que cada query que tocaba productos timeoutee, aunque los datos de usuario y orden estaban listos en 50ms. Un timeout de 2 segundos en las llamadas a subgraphs lo arregló.

Cuando un campo tiene que desaparecer del supergraph, deprecalo en el subgraph que lo posee y manejá la eliminación con un cronograma documentado — la Plantilla de Política de Deprecación de GraphQL es una política lista para copiar para eso, con seguimiento de uso y criterios de retiro incluidos.

Usá DataLoader para el batching de entidades. Sin eso, resolver una lista de 50 órdenes dispara 50 llamadas separadas al subgraph de Products. DataLoader las batchea en una sola llamada. Este es el mayor win de performance individual en federation. Si no hacés nada más después de leer esta guía, añadí DataLoader a tus resolvers de entidades.

Errores Comunes

Definir el mismo campo en múltiples subgraphs sin @shareable hace fallar la composición. Olvidar implementar __resolve_reference deja las búsquedas de entidades retornando null. Acoplar demasiado los subgraphs anula el propósito de federation, porque los equipos vuelven a depender de los internos de cada uno. Una vez revisé un PR donde un developer añadió un campo User.email al subgraph de Orders “solo para evitar una llamada extra.” Eso es exactamente lo que federation está diseñado para prevenir. El subgraph de Orders debería devolver una referencia de entidad y dejar que el subgraph de Users resuelva el email.

No manejar el downtime de un subgraph hace que el gateway devuelva un error en lugar de datos parciales. Usar @requires sobre un campo que no está marcado como @external falla la validación. Saltearse los tests locales de composición deja que los conflictos de schema lleguen a producción. Corré rover supergraph compose localmente antes de pushear a CI. Toma 5 segundos y atrapa la mayoría de los errores de composición.

Abusar de @shareable difumina los límites de ownership. Ignorar el rendimiento de los query plans puede convertir una query en una secuencia N+1 de resoluciones de entidades. Exponer IDs internos a través de los límites de los subgraphs filtra detalles de implementación. Por último, no usar DataLoader para el batching de entidades puede hacer que una sola query del cliente dispare cientos de llamadas a subgraphs. Profileé una query federada una vez que hizo 127 llamadas a subgraphs para una lista de 50 órdenes. Cada orden disparaba una llamada separada al subgraph de Products. Cambiar a DataLoader las batcheó en una sola llamada y cortó el tiempo de query de 3 segundos a 200ms. El fix tomó 15 minutos y salvó cada query que tocaba productos desde ese día.

Resumen

GraphQL Federation divide un schema monolítico en subgraphs que los equipos poseen y despliegan de forma independiente. El gateway los compone en un solo API, construye query plans y enruta cada parte de una query al subgraph correcto. Las entidades (@key, @extends, @external) son la cola que deja a los subgraphs referenciarse entre sí sin acoplamiento. Usá Rover para componer el supergraph en CI, cacheá la resolución de entidades en el gateway y configurá timeouts en cada llamada a subgraph. Si no te acordás nada más: un subgraph por equipo, entidades para referencias cross-subgraph y datos parciales sobre fallo total.

Vengo usando federation en producción hace más de tres años. El mayor beneficio no es técnico, es organizacional. Los equipos pueden shippear cambios de schema sin coordinar con cada otro equipo. El mayor costo es operacional: ahora corrés tres o más servicios GraphQL en lugar de uno. Empezá chico, con dos o tres subgraphs, y dividí más solo cuando la estructura del equipo lo exija. Empecé con dos subgraphs y crecí a siete a lo largo de dos años a medida que el equipo creció.

Ver También

Preguntas frecuentes

¿Cuál es la diferencia entre schema stitching y federation?

Schema stitching combina schemas a mano con resolvers custom. Federation usa un protocolo estandarizado (@key, @extends y __resolveReference) así que los subgraphs declaran sus relaciones de forma declarativa. Para proyectos nuevos, federation es la mejor opción porque es más mantenible y tiene mejor tooling. Migré un proyecto de stitching a federation en 2020 y no volví atrás. El mayor win fue no tener que escribir merge resolvers custom para cada type.

¿Cómo maneja el gateway una query que abarca múltiples subgraphs?

El gateway construye un query plan. Para una query que trae un usuario y sus órdenes, primero llama al subgraph de Users, luego usa el id del usuario como entity key para llamar al subgraph de Orders. Une los resultados y retorna una sola response al cliente. El query plan se ve en Apollo Studio, lo que te ayuda a entender el costo de cada query.

¿Puedo usar federation sin Apollo?

Sí. Federation es una spec abierta. Podés usar Apollo Gateway (Node.js), Apollo Router (Rust) o un gateway custom. El protocolo de federation es language-agnostic, así que cualquier servidor GraphQL que lo use funciona. Podés construir subgraphs en Python (Ariadne, Strawberry), Java (DGS), Go (gqlgen) y Ruby (graphql-ruby). Mixé subgraphs de Node.js y Python en el mismo grafo federado sin issues.

¿Cuándo prefiero una API GraphQL monolítica sobre federation?

Federation vale la pena cuando múltiples equipos poseen distintas partes del schema y necesitan desplegar de forma independiente. Si tu API es chica, tiene un único dueño y pocos puntos de acoplamiento, un schema monolítico es más simple y tiene menos overhead. Generalmente recomiendo federation cuando tenés tres o más equipos contribuyendo al mismo API de GraphQL. Por debajo de eso, el overhead de infraestructura no vale la pena.

¿Cómo manejo la autenticación en un grafo federado?

Manejá la auth a nivel del gateway, no en cada subgraph. El gateway valida el token, extrae el contexto del usuario y lo pasa a los subgraphs vía headers de request. Los subgraphs confían en el gateway y usan el contexto para autorizar acceso. Esto evita duplicar lógica de auth entre subgraphs y mantiene al gateway como el único punto de enforcement.