tutoriales.com

Federación GraphQL: Construyendo APIs Distribuidas y Escalables con Apollo Federation

Este tutorial te guiará a través de la implementación de GraphQL Federation utilizando Apollo Federation. Descubrirás cómo construir un grafo de datos unificado a partir de servicios independientes, mejorando la escalabilidad, mantenibilidad y colaboración en equipos grandes.

Intermedio20 min de lectura19 views
Reportar error

🚀 Introducción a la Federación GraphQL

En el mundo del desarrollo de software moderno, la tendencia hacia arquitecturas de microservicios es innegable. Estas arquitecturas ofrecen beneficios como la escalabilidad independiente, la autonomía de los equipos y la resiliencia. Sin embargo, cuando se trata de construir una API, integrar datos de múltiples microservicios puede volverse un desafío. Aquí es donde GraphQL Federation entra en juego, ofreciendo una solución elegante para unificar estas APIs dispersas en un solo grafo de datos global.

Tradicionalmente, con una arquitectura de microservicios, cada servicio podría exponer su propia API REST o GraphQL. Esto obligaría a los clientes a realizar múltiples llamadas a diferentes servicios y a ensamblar los datos por sí mismos, lo que aumenta la complejidad en el lado del cliente y dificulta la evolución de la API.

GraphQL Federation, popularizada por Apollo, permite que cada microservicio implemente una parte de tu esquema GraphQL global (un subgrafo), mientras que un gateway se encarga de componer y resolver las consultas de los clientes. El resultado es un grafo de datos unificado y coherente, donde los clientes solo interactúan con un único endpoint GraphQL.

¿Por qué Federación GraphQL? 🤔

La Federación GraphQL no es solo una moda; resuelve problemas reales y ofrece ventajas significativas:

  • Escalabilidad: Cada subservicio puede escalar de forma independiente.
  • Mantenibilidad: Los equipos son dueños de sus propios subgrafos, facilitando el desarrollo y despliegue.
  • Colaboración: Diferentes equipos pueden trabajar en partes separadas del grafo sin colisiones.
  • Flexibilidad del Cliente: Los clientes obtienen un único endpoint y un esquema cohesivo.
  • Rendimiento: El gateway puede optimizar la resolución de consultas entre servicios.
🔥 **Importante:** La Federación GraphQL es una evolución de los patrones de composición de esquemas GraphQL. Se diferencia de la *composición de esquemas tradicional* en que permite que los subgrafos *compartan tipos* y *extiendan tipos* definidos por otros, creando un grafo verdaderamente distribuido.

🛠️ Conceptos Clave en Apollo Federation

Para entender cómo funciona Apollo Federation, es crucial familiarizarse con algunos conceptos fundamentales:

1. El Gateway (API Gateway) 🚪

El Gateway es el punto de entrada para todas las consultas de los clientes. Es responsable de:

  • Recibir las consultas GraphQL de los clientes.
  • Analizar la consulta para determinar qué subservicios son necesarios para resolverla.
  • Distribuir las partes relevantes de la consulta a los subservicios apropiados.
  • Componer las respuestas de los subservicios en una única respuesta GraphQL coherente para el cliente.

El Gateway actúa como un orquestador inteligente, ocultando la complejidad de la arquitectura de microservicios al cliente.

2. Los Subgrafos (Subgraphs) 🌿

Cada microservicio expone una porción de tu esquema GraphQL global. Esta porción es lo que llamamos un subgrafo. Un subgrafo es una API GraphQL estándar con algunas directivas Federation especiales que le indican al Gateway cómo interactuar con él.

Cada subgrafo es responsable de su propio dominio de datos y de resolver las consultas relacionadas con ese dominio. Por ejemplo, un servicio de Usuarios tendría un subgrafo que define el tipo User, y un servicio de Productos tendría un subgrafo que define el tipo Product.

3. Tipos extend y la Directiva @key 🔑

Aquí es donde la Federación se vuelve realmente poderosa. La directiva @key se usa para identificar un campo o conjunto de campos que actúan como una clave única para un tipo de entidad en un subgrafo. Esto permite que el Gateway sepa cómo identificar y referenciar ese tipo en otros subgrafos.

Por ejemplo, el servicio de Productos podría querer incluir información sobre el creador de un producto. En lugar de duplicar los datos del usuario, puede extender el tipo User (definido en el servicio de Usuarios) y agregar campos específicos del dominio del producto a ese tipo, haciendo referencia a la clave id del usuario.

# En el subgrafo de Usuarios:
type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
}

# En el subgrafo de Productos:
extend type User @key(fields: "id") {
  id: ID! @external
  products: [Product!]
}

type Product @key(fields: "sku") {
  sku: String!
  name: String!
  price: Float!
  createdBy: User!
}

En este ejemplo:

  • User es definido en el subgrafo de Usuarios y su clave es id.
  • El subgrafo de Productos extiende el tipo User, indicando que tiene un campo id que es external (externo) y que lo usará para obtener más información sobre el usuario del servicio de Usuarios.
  • El campo products en el User extendido es resuelto por el subgrafo de Productos.

4. Directivas Federation Fundamentales 📖

Además de @key y extend, otras directivas clave incluyen:

  • @external: Marca un campo como definido en otro subgrafo, indicando que el subgrafo actual no lo resolverá directamente.
  • @requires: Indica que un campo solo se puede resolver si otros campos (marcados con @external) también están presentes en la consulta. Esto asegura que el subgrafo tenga todos los datos necesarios para resolver un campo.
  • @provides: Usada en campos de entidades para indicar que un subgrafo puede proporcionar un campo que normalmente sería resuelto por otro subgrafo. Esto puede optimizar consultas evitando viajes de ida y vuelta adicionales.
  • @shareable: Permite que un campo se defina en múltiples subgrafos si sus implementaciones son idénticas. (Introducida en Federation 2).
  • @override: Permite que un subgrafo reemplace la implementación de un campo de una entidad definido en otro subgrafo. (Introducida en Federation 2).
📌 **Nota:** Las directivas de Federación son procesadas por el Gateway durante la composición del esquema y en tiempo de ejecución para enrutar y resolver las consultas.

🏗️ Creando un Proyecto de Federación GraphQL: Paso a Paso

Vamos a construir un ejemplo práctico de Federación GraphQL. Necesitaremos al menos dos subgrafos (servicios) y un Gateway.

Nuestro Escenario:

Imaginemos una plataforma de comercio electrónico. Tendremos dos microservicios principales:

  1. Servicio de Usuarios: Gestiona usuarios (ID, nombre, email).
  2. Servicio de Productos: Gestiona productos (ID, nombre, precio, descripción) y el usuario que lo publicó.

El Gateway unificará estos dos servicios.

Pre-requisitos ✅

  • Node.js (versión LTS recomendada)
  • npm o yarn
  • Un editor de código (VS Code es una excelente opción)

1. Inicializar el Proyecto Principal 📁

Crea una carpeta para tu proyecto y entra en ella:

mkdir graphql-federation-example
cd graphql-federation-example
npm init -y

2. Crear los Subgrafos (Microservicios) 🧱

Crearemos dos subgrafos, users y products, usando Apollo Server.

2.1. Subgrafo de Usuarios (services/users) 👤

mkdir services
mkdir services/users
cd services/users
npm init -y
npm install apollo-server @apollo/subgraph graphql

Crea el archivo index.js en services/users:

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

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

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

const users = [
  { id: '1', name: 'Alice', email: 'alice@example.com' },
  { id: '2', name: 'Bob', email: 'bob@example.com' },
  { id: '3', name: 'Charlie', email: 'charlie@example.com' },
];

const resolvers = {
  Query: {
    user: (_, { id }) => users.find(user => user.id === id),
    users: () => users,
  },
  User: {
    __resolveReference(object) {
      return users.find(user => user.id === object.id);
    },
  },
};

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

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

Explicación:

  • @key(fields: "id"): Declara que User es una entidad y su clave única es id.
  • extend type Query: Extiende el tipo Query raíz para agregar consultas específicas de usuarios.
  • __resolveReference: Una función especial de Federación que el Gateway usa para resolver referencias a tipos extendidos. Si el Gateway necesita información sobre un User (por ejemplo, por su id), llamará a esta función en el subgrafo de Usuarios para obtener los datos completos del usuario.

2.2. Subgrafo de Productos (services/products) 📦

cd ../products # Volvemos a la carpeta services y creamos products
mkdir services/products
cd services/products
npm init -y
npm install apollo-server @apollo/subgraph graphql

Crea el archivo index.js en services/products:

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

const typeDefs = gql`
  extend type Query {
    product(sku: String!): Product
    products: [Product!]
  }

  type Product @key(fields: "sku") {
    sku: String!
    name: String!
    price: Float!
    description: String
    createdBy: User!
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    products: [Product!]
  }
`;

const products = [
  { sku: 'P1', name: 'Laptop', price: 1200, description: 'Potente laptop', createdBy: '1' },
  { sku: 'P2', name: 'Mouse', price: 25, description: 'Mouse ergonómico', createdBy: '1' },
  { sku: 'P3', name: 'Teclado', price: 75, description: 'Teclado mecánico', createdBy: '2' },
];

const resolvers = {
  Query: {
    product: (_, { sku }) => products.find(product => product.sku === sku),
    products: () => products,
  },
  Product: {
    createdBy: (product) => ({
      __typename: "User",
      id: product.createdBy,
    }),
  },
  User: {
    products: (user) => products.filter(product => product.createdBy === user.id),
  },
};

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

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

Explicación:

  • type Product @key(fields: "sku"): Product es una entidad con sku como clave.
  • extend type User @key(fields: "id"): El subgrafo de Productos extiende el tipo User definido en el subgrafo de Usuarios. Declara que su id es externo.
  • createdBy: User!: El campo createdBy en Product es de tipo User!. El resolver para createdBy devuelve un stub de User (solo con __typename y id), y el Gateway se encargará de resolver el resto de los campos de User consultando al subgrafo de Usuarios.
  • User.products: Este resolver se ejecuta cuando se consulta el campo products en un User (que ha sido extendido por este subgrafo). Filtra los productos que pertenecen a ese usuario.

3. Crear el Gateway 🌐

El Gateway unirá ambos subgrafos. Volvemos a la carpeta raíz del proyecto (graphql-federation-example).

cd ../../ # Volvemos a graphql-federation-example
npm install @apollo/gateway apollo-server graphql

Crea el archivo gateway.js en la raíz del proyecto:

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

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001' },
      { name: 'products', url: 'http://localhost:4002' },
    ],
  }),
});

(async () => {
  const server = new ApolloServer({
    gateway,
    // Para Apollo Studio / Playground
    // Desactiva la introspección y el playground en producción
    apollo: {
      graphRef: 'your-graph-id@current',
    },
  });

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

Explicación:

  • ApolloGateway: La clase principal para configurar el Gateway.
  • IntrospectAndCompose: Una utilidad que permite al Gateway introspectar automáticamente los esquemas de los subgrafos en tiempo de ejecución y componer el supergrafo. Para producción, se recomienda un supergraphSdl estático o generado previamente para mayor estabilidad.
  • subgraphs: Un array que lista todos los subgrafos que el Gateway debe conocer, con sus nombres y URLs.

🏃 Ejecutando y Probando la Federación

Ahora que tenemos todos los componentes, es hora de ponerlos en marcha.

1. Iniciar los Subgrafos 🟢

Abre dos terminales separadas y, en cada una, navega a la carpeta de un subgrafo y ejecútalo:

Terminal 1 (Subgrafo de Usuarios):

cd services/users
node index.js

Deberías ver: 🚀 User Subgraph ready at http://localhost:4001/

Terminal 2 (Subgrafo de Productos):

cd services/products
node index.js

Deberías ver: 🚀 Product Subgraph ready at http://localhost:4002/

2. Iniciar el Gateway 🟢

Abre una tercera terminal, navega a la carpeta raíz del proyecto y ejecuta el Gateway:

Terminal 3 (Gateway):

node gateway.js

Deberías ver: 🚀 Gateway ready at http://localhost:4000/

💡 **Consejo:** Si usas `nodemon`, puedes configurarlo para reiniciar automáticamente los servicios cuando cambien los archivos, lo que es útil durante el desarrollo.

3. Probar con Consultas GraphQL 🧪

Accede al Apollo Playground (o interfaz GraphQL de tu preferencia) en http://localhost:4000/.

Aquí puedes ejecutar consultas que combinan datos de ambos servicios:

Consulta 1: Obtener un usuario y sus productos

query GetUserWithProducts {
  user(id: "1") {
    id
    name
    email
    products {
      sku
      name
      price
    }
  }
}

Resultado esperado:

{
  "data": {
    "user": {
      "id": "1",
      "name": "Alice",
      "email": "alice@example.com",
      "products": [
        {
          "sku": "P1",
          "name": "Laptop",
          "price": 1200
        },
        {
          "sku": "P2",
          "name": "Mouse",
          "price": 25
        }
      ]
    }
  }
}
📌 **Nota:** Observa cómo una única consulta al Gateway obtiene datos de `User` (del subgrafo de Usuarios) y luego, para el campo `products`, el Gateway consulta el subgrafo de Productos, pasándole el `id` del usuario.

Consulta 2: Obtener un producto y el nombre de su creador

query GetProductWithCreator {
  product(sku: "P3") {
    sku
    name
    price
    description
    createdBy {
      id
      name
    }
  }
}

Resultado esperado:

{
  "data": {
    "product": {
      "sku": "P3",
      "name": "Teclado",
      "price": 75,
      "description": "Teclado mecánico",
      "createdBy": {
        "id": "2",
        "name": "Bob"
      }
    }
  }
}

En este caso, el Gateway primero consulta el subgrafo de Productos para obtener la información del producto, incluyendo el id del createdBy. Luego, usa ese id para consultar el subgrafo de Usuarios y obtener el name del usuario Bob.


📊 Funcionamiento Interno del Gateway (Ejemplo Visual)

El Gateway es el cerebro detrás de la Federación. Cuando recibe una consulta, realiza una serie de pasos:

  1. Análisis de la Consulta: El Gateway parsea la consulta GraphQL para entender qué datos se solicitan.
  2. Planificación de la Ejecución: Basándose en el esquema supergrafo (compuesto de todos los subgrafos), el Gateway determina qué subgrafos deben ser consultados y en qué orden.
  3. Ejecución Paralela/Secuencial: Envía las consultas parciales a los subgrafos correspondientes. Puede ejecutar consultas en paralelo si no hay dependencias, o secuencialmente si un subgrafo necesita la respuesta de otro.
  4. Composición de la Respuesta: Una vez que recibe las respuestas de todos los subgrafos, las une en una única respuesta GraphQL que envía al cliente.
Cliente Gateway Subgrafo Usuarios Subgrafo Productos Consulta GraphQL Respuesta Unificada Consulta Parcial 1 Consulta Parcial 2

Ejemplo de Plan de Ejecución para GetUserWithProducts:

  1. El Gateway recibe query { user(id: "1") { id name email products { sku name } } }.
  2. Identifica que user(id: "1") { id name email } necesita ser resuelto por el Subgrafo de Usuarios.
  3. Identifica que products { sku name } necesita ser resuelto por el Subgrafo de Productos, pero requiere el id del User.
  4. Paso 1: Envía query { user(id: "1") { id name email } } al Subgrafo de Usuarios. Recibe { "id": "1", "name": "Alice", "email": "alice@example.com" }.
  5. Paso 2: Con el id (1) del usuario, envía query { _entities(representations: [{ __typename: "User", id: "1" }]) { ... on User { products { sku name } } } } al Subgrafo de Productos. El Subgrafo de Productos resuelve los productos para el usuario con id: "1".
  6. Paso 3: Combina las respuestas de ambos subgrafos en una única respuesta para el cliente.
Proceso Completado

📈 Consideraciones Avanzadas y Mejores Prácticas

La Federación GraphQL es una herramienta potente, pero su implementación óptima requiere considerar algunos aspectos avanzados.

1. Gestión de Errores y Retries ⚠️

En un entorno distribuido, los fallos de red o de servicio son inevitables. El Gateway debe ser robusto y capaz de manejar estas situaciones. Implementar reintentos (retries) con retroceso exponencial y un circuito de interrupción (circuit breaker) puede mejorar la resiliencia.

2. Autenticación y Autorización 🔒

La autenticación generalmente se maneja en el Gateway. El token de autenticación del cliente (por ejemplo, JWT) se valida en el Gateway y luego se propaga a los subgrafos a través de los headers HTTP o un contexto específico. Cada subgrafo es entonces responsable de aplicar sus propias reglas de autorización basadas en la información recibida.

// Ejemplo de contexto en el Gateway para propagar headers
const gateway = new ApolloGateway({
  // ...
  buildService({
ame, url}) {
    return new RemoteGraphQLDataSource({
      url,
      willSendRequest({ request, context }) {
        // Propagar el token de autorización del cliente a los subgrafos
        if (context.token) {
          request.http.headers.set('authorization', context.token);
        }
      },
    });
  },
});

const server = new ApolloServer({
  gateway,
  context: ({ req }) => {
    return { token: req.headers.authorization };
  },
});

3. Rendimiento y Caching 🏎️

  • Caching a nivel de Gateway: Implementa caching para respuestas de consultas comunes. Apollo proporciona herramientas para esto.
  • Caching a nivel de Subgrafo: Cada subgrafo puede implementar su propia estrategia de caching para sus datos internos (por ejemplo, Redis).
  • Batching y Dataloader: Asegúrate de que tus subgrafos utilicen patrones como DataLoader para agrupar consultas y evitar el problema N+1, especialmente cuando se resuelven relaciones (createdBy o products en nuestro ejemplo).

4. Monitoreo y Observabilidad 👀

Es crucial monitorear el rendimiento del Gateway y de cada subgrafo. Herramientas como Apollo Studio (para el supergrafo), Prometheus/Grafana (para métricas) y Jaeger/OpenTelemetry (para trazas distribuidas) son esenciales para diagnosticar problemas en un sistema federado.

5. Federación 2.0 ✨

Apollo Federation 2 introdujo mejoras significativas, como:

  • @shareable: Permite definir el mismo campo en varios subgrafos si tiene la misma implementación.
  • @override: Permite a un subgrafo reemplazar la implementación de un campo de otro subgrafo.
  • Mejoras en la composición de esquemas y la gestión de entidades.

Es recomendable usar Federation 2.x para nuevos proyectos debido a estas mejoras.

6. Despliegue y CI/CD 🚀

Considera cómo desplegarás tus subgrafos y tu Gateway. Cada subgrafo puede desplegarse y versionarse independientemente. El Gateway necesitará acceso a las URLs de los subgrafos, lo que puede manejarse a través de variables de entorno o un sistema de descubrimiento de servicios.

¿Cuál es la diferencia entre Federation 1 y Federation 2? Federation 2 introduce mejoras clave como las directivas `@shareable` y `@override`, que ofrecen más flexibilidad en la composición de esquemas y la colaboración entre equipos. También mejora el rendimiento y la experiencia del desarrollador con una composición de esquemas más robusta y herramientas de desarrollo avanzadas.

🏁 Conclusión

La Federación GraphQL es un cambio de paradigma en cómo construimos y gestionamos APIs en arquitecturas de microservicios. Permite a los equipos trabajar de forma independiente en sus dominios de datos, mientras que el Gateway unifica estas piezas en un grafo de datos global y coherente, fácil de consumir para los clientes.

Si bien la configuración inicial puede parecer un poco más compleja que una API monolítica, los beneficios a largo plazo en términos de escalabilidad, mantenibilidad y autonomía de equipo son inmensos. Al seguir este tutorial, has dado tus primeros pasos para dominar esta poderosa tecnología y construir APIs GraphQL de próxima generación.

¡Experimenta con el código, extiende los ejemplos y lleva tus habilidades de GraphQL al siguiente nivel con Apollo Federation!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!