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.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
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
FAQ
Schema stitching vs federation — cual usar?
Federation (Apollo Federation) es el estandar moderno para GraphQL distribuido. Usa stitching cuando tienes servidores no-Apollo, esquemas legacy, o necesitas merging mas ligero sin el runtime de federation.
Puedo stitcheear APIs REST?
Si. Envuelve endpoints REST como subesquemas GraphQL usando @graphql-tools/wrap y un executor personalizado que traduce operaciones GraphQL a llamadas HTTP.
Como maneja stitching la autenticacion?
El gateway extrae tokens de auth de las peticiones entrantes y los reenvia a los subesquemas via el context del executor. Cada subesquema aplica sus propias reglas de autorizacion.
Que hay del rendimiento?
Cada delegacion anade un round-trip de red. Usa batching, caching y DataLoader a nivel de gateway para minimizar llamadas. Para escenarios de alto trafico, considera federation con su query planning.
¿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
Federated Identity Pattern
Delegate authentication to external identity providers. A pattern for integrating OAuth2, OIDC, SAML, and SSO across multiple services and organizations.
PatternGraphQL Batched Resolver Pattern
Resolve nested GraphQL fields in a single batched request to eliminate N+1 queries and reduce database load.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
PatternGraphQL Federated Entity Pattern
Share entity types across federated GraphQL services so the gateway can resolve fields from multiple subgraphs transparently.
PatternGraphQL Interface Polymorphism Pattern
Model polymorphic types with GraphQL interfaces to share field contracts across different object types while keeping resolvers type-specific.