Patron de Schema Stitching en GraphQL
Combina multiples esquemas GraphQL independientes en un unico esquema unificado que los clientes pueden consultar como un solo grafo.
Descripcion general
Schema stitching fusiona multiples esquemas GraphQL independientes en un unico esquema unificado. Cada subesquema es propietario de sus tipos y resolvers. El gateway stitcheado delega la resolucion de campos al servicio de origen. Los clientes consultan un endpoint y ven un solo grafo, mientras internamente el trabajo se distribuye a multiples servicios.
Esto difiere de federation: stitching trabaja a nivel de esquema (fusionando definiciones de tipos), mientras que federation trabaja a nivel de servicio (cada servicio contribuye con porciones de un grafo compartido). Stitching es mas ligero pero requiere configuracion manual de merge.
Cuando Usar
-
For alternatives, see GraphQL Error Extension Pattern.
-
Multiples equipos poseen APIs GraphQL separadas que necesitan un punto de entrada unificado
-
Migracion gradual de un esquema GraphQL monolitico a servicios distribuidos
-
Agregacion de APIs GraphQL de terceros detras de un unico gateway
-
Cuando federation no es factible (servidores no-Apollo, esquemas legacy)
Cuando No Usar
- Proyectos greenfield donde federation esta disponible desde el inicio
- Esquemas con conflictos de tipos pesados que requieren reglas de merge complejas
- Equipos que necesitan aislamiento estricto en tiempo de ejecucion entre servicios
Solucion
1. Definir Subesquemas
Cada servicio expone su propio esquema de forma independiente.
Servicio de Usuarios (users-api)
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
Servicio de Posts (posts-api)
type Post {
id: ID!
title: String!
body: String!
authorId: ID!
}
type Query {
post(id: ID!): Post
posts: [Post!]!
}
2. Stitch con @graphql-tools/stitch
import { makeExecutableSchema } from '@graphql-tools/schema';
import { stitchSchemas } from '@graphql-tools/stitch';
import { createExecutor } from '@graphql-tools/executor-apollo-link';
import { ApolloLink, HttpLink } from '@apollo/client/core';
// Crear executors para cada subesquema
const usersExecutor = createExecutor(
new ApolloLink(new HttpLink({ uri: 'http://localhost:4001/graphql' }))
);
const postsExecutor = createExecutor(
new ApolloLink(new HttpLink({ uri: 'http://localhost:4002/graphql' }))
);
// Definir configs de subesquema
const usersSubschema = {
schema: makeExecutableSchema({
typeDefs: usersTypeDefs,
resolvers: usersResolvers,
}),
executor: usersExecutor,
};
const postsSubschema = {
schema: makeExecutableSchema({
typeDefs: postsTypeDefs,
resolvers: postsResolvers,
}),
executor: postsExecutor,
};
3. Agregar Tipos Fusionados con Delegacion de Campos
El gateway extiende User para incluir posts y Post para incluir author. Los resolvers de campos delegan al subesquema propietario.
const linkTypeDefs = `
extend type User {
posts: [Post!]!
}
extend type Post {
author: User!
}
`;
const gatewaySchema = stitchSchemas({
subschemas: [usersSubschema, postsSubschema],
typeDefs: linkTypeDefs,
resolvers: {
User: {
posts: {
selectionSet: '{ id }',
resolve: (user, _args, context, info) => {
return info.mergeInfo.delegateToSchema({
schema: postsSubschema,
operation: 'query',
fieldName: 'posts',
args: { authorId: user.id },
context,
info,
});
},
},
},
Post: {
author: {
selectionSet: '{ authorId }',
resolve: (post, _args, context, info) => {
return info.mergeInfo.delegateToSchema({
schema: usersSubschema,
operation: 'query',
fieldName: 'user',
args: { id: post.authorId },
context,
info,
});
},
},
},
},
});
4. Iniciar el Gateway
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({ schema: gatewaySchema });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Gateway listo en ${url}`);
5. Consultar el Grafo Unificado
Los clientes consultan ambos servicios a traves de una sola peticion:
query {
user(id: "1") {
name
email
posts {
title
body
}
}
}
El gateway resuelve name y email desde el servicio de usuarios, luego delega posts al servicio de posts usando el campo id del usuario.
Explicacion
- Subesquemas: Cada servicio funciona independientemente con su propio esquema y resolvers
- Executor: Envia operaciones al endpoint del subesquema remoto via HTTP
- Tipos de enlace: Extensiones de tipo (
extend type User { posts: [Post!]! }) crean relaciones entre servicios - Delegacion:
delegateToSchemareenvia una resolucion de campo al subesquema propietario, pasando los campos del objeto padre como argumentos - Selection set: Le dice al stitcher que campos del objeto padre se necesitan para la delegacion (
{ id }paraUser.posts,{ authorId }paraPost.author)
Variantes
Delegacion Batch
En lugar de resolver posts por usuario, agrupa la busqueda:
User: {
posts: {
selectionSet: '{ id }',
resolve: async (user, _args, context, info) => {
const result = await info.mergeInfo.delegateToSchema({
schema: postsSubschema,
operation: 'query',
fieldName: 'postsByAuthorIds',
args: { authorIds: [user.id] },
context,
info,
});
return result;
},
},
},
Para listas de usuarios, usa un resolver batch que recolecta todos los valores de authorId y hace una sola peticion.
Campos Calculados
Agrega campos que no existen en ningun subesquema pero se calculan en el gateway:
const gatewaySchema = stitchSchemas({
subschemas: [usersSubschema, postsSubschema],
resolvers: {
User: {
postCount: {
selectionSet: '{ id }',
resolve: async (user, _args, context, info) => {
const posts = await info.mergeInfo.delegateToSchema({
schema: postsSubschema,
operation: 'query',
fieldName: 'posts',
args: { authorId: user.id },
context,
info,
});
return posts.length;
},
},
},
},
});
Type Merging
Para esquemas que comparten un tipo (ej. ambos servicios definen User), configura reglas de merge:
const usersSubschema = {
schema: usersSchema,
executor: usersExecutor,
merge: {
User: {
selectionSet: '{ id }',
fieldName: 'user',
args: (originalResult) => ({ id: originalResult.id }),
},
},
};
Mejores Practicas
- Mantener los subesquemas pequenos y enfocados en un dominio
- Usar
selectionSeten cada campo fusionado para evitar over-fetching - Batchear delegaciones al resolver campos de lista para evitar llamadas N+1
- Cachear resultados del executor cuando sea posible para reducir latencia del gateway
- Documentar que servicio posee que tipo para evitar conflictos de merge
- Monitorear latencia de delegacion — el gateway anade un hop de red por delegacion
Errores Comunes
- Falta
selectionSet: Sin el, el stitcher puede no tener los campos necesarios para la delegacion, causando resultados null - Dependencias circulares: Servicio A extiende el tipo de Servicio B, Servicio B extiende el tipo de Servicio A — funciona pero crea bucles infinitos de resolucion si no se tiene cuidado
- Campos de query raiz superpuestos: Dos subesquemas que definen
Query.user(id: ID!)— configurar reglas de merge o renombrar uno - No batchear delegaciones de lista: Resolver
postspara 50 usuarios uno por uno causa 50 peticiones al servicio de posts - Ignorar propagacion de errores: Los errores de los subesquemas necesitan mapeo adecuado a nivel de gateway
Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Patrón Federated Identity
Delega la autenticación a proveedores de identidad externos. Un patrón para integrar OAuth2, OIDC, SAML y SSO entre múltiples servicios y organizaciones.
PatternPatron de Resolver Batch en GraphQL
Resuelve campos anidados de GraphQL en una sola peticion batch para eliminar consultas N+1 y reducir la carga de la base de datos.
GuideGuía Completa de GraphQL Federation
Construye APIs GraphQL unificadas con Apollo Federation. Cubre subgraphs, supergraph, resolución de entidades y despliegue de gateway.
GuideGuía Completa de GraphQL Federation
Construye APIs GraphQL unificadas con Apollo Federation. Cubre subgraphs, supergraph, resolución de entidades y despliegue de gateway.
PatternPatrón de entidades federadas en GraphQL
Compartí una entidad entre subgraphs de Apollo Federation. Usá @key, @external y @shareable para que cada servicio maneje los campos que conoce.
PatternPatron de Polimorfismo con Interfaces en GraphQL
Modela tipos polimorficos con interfaces GraphQL para compartir contratos de campos entre diferentes tipos de objeto manteniendo resolvers especificos.