Patrón de Schema Stitching en GraphQL
Combina múltiples esquemas GraphQL independientes en un único esquema unificado que los clientes pueden consultar como un solo grafo.
Descripción general
Tu equipo de usuarios publica una API GraphQL. Tu equipo de posts publica otra. El producto quiere un único endpoint donde los clientes pidan un usuario y sus posts en una sola query — y ningún equipo quiere fusionar codebases. Schema stitching resuelve exactamente eso: tomas dos esquemas independientes, fusionas sus definiciones de tipos en un esquema gateway, y el gateway delega cada campo al servicio que lo posee.
Cada subesquema conserva sus propios tipos y resolvers, así que los equipos despliegan de forma independiente. El gateway añade tipos de enlace (extend type User { posts: [Post!]! }) que crean relaciones entre servicios, y luego reenvía la resolución de campos vía delegateToSchema. Los clientes ven un solo grafo; internamente, el trabajo se distribuye a los 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 aporta porciones de un grafo compartido, coordinadas por un query planner). Stitching es más ligero pero exige configurar el merge a mano — el trade-off se detalla abajo y en la guía de GraphQL federation.
Cuándo usarlo
- Dos o más equipos poseen APIs GraphQL separadas que necesitan un punto de entrada unificado
- Migración gradual de un esquema GraphQL monolítico a servicios distribuidos
- Agregación de APIs GraphQL de terceros detrás de un único gateway
- Cuando federation no es factible (servidores no-Apollo, esquemas legacy que no puedes modificar)
Cuándo no usarlo
- Proyectos greenfield donde federation está disponible desde el inicio — el query planner y las herramientas de composición te ahorran trabajo manual
- Esquemas con conflictos de tipos pesados que requieren reglas de merge complejas
- Equipos que necesitan aislamiento estricto en tiempo de ejecución entre servicios (el gateway es un proceso compartido)
Solución
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. Unir con @graphql-tools/stitch
import { makeExecutableSchema } from '@graphql-tools/schema';
import { stitchSchemas } from '@graphql-tools/stitch';
import { buildHTTPExecutor } from '@graphql-tools/executor-http';
// Crear executors para cada subesquema
const usersExecutor = buildHTTPExecutor({
endpoint: 'http://localhost:4001/graphql',
});
const postsExecutor = buildHTTPExecutor({
endpoint: 'http://localhost:4002/graphql',
});
// Definir la config de cada subesquema
const usersSubschema = {
schema: makeExecutableSchema({
typeDefs: usersTypeDefs,
resolvers: usersResolvers,
}),
executor: usersExecutor,
};
const postsSubschema = {
schema: makeExecutableSchema({
typeDefs: postsTypeDefs,
resolvers: postsResolvers,
}),
executor: postsExecutor,
};
3. Añadir tipos fusionados con delegación de campos
El gateway extiende User para incluir posts y Post para incluir author. Los resolvers de campo 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) => {
// Delegar al subesquema de posts
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 través de una sola petición:
query {
user(id: "1") {
name
email
posts {
title
body
}
}
}
El gateway resuelve name y email desde el servicio de usuarios, y luego delega posts al servicio de posts usando el campo id del usuario.
Explicación
Traza esa query por el gateway. Parsea { user { name email posts { title } } } contra el esquema unido. user, name y email pertenecen al subesquema de usuarios, así que el executor reenvía esa parte de la operación por HTTP. Cuando vuelve la respuesta, el gateway ve que posts no es un campo de users-api — vive en los tipos de enlace — así que se dispara el resolver de User.posts. delegateToSchema construye una operación nueva posts(authorId: "1") para el subesquema de posts, usando los campos del selectionSet: '{ id }' del objeto padre. El gateway fusiona ambos resultados parciales y devuelve un solo JSON.
Las piezas implicadas:
- Subesquemas: servicios independientes, cada uno con su propio esquema y resolvers. El gateway enruta operaciones; nunca ejecuta su lógica de negocio.
- Executor: lo que envía la operación al endpoint remoto. HTTP cubre casi todos los casos, pero vale cualquier cosa que acepte una operación GraphQL.
- Tipos de enlace: bloques
extend typeque acoplan campos de un servicio al tipo de otro — el servicio propietario ni se entera. selectionSet: el contrato entre padre y campo delegado — qué campos debe traer el stitcher del padre para que la delegación tenga sus argumentos. Olvidarlo es la causa más común de campos fusionados que vuelvennull.delegateToSchema: reenvía la resolución de un campo al subesquema propietario y fusiona el resultado en el padre.
Errores y autenticación
Los errores de un subesquema suben por el array errors del gateway junto a data, así que un servicio de posts caído no derriba los datos de usuario — pero conviene mapearlos y anotarlos en el gateway (el patrón de error extension en GraphQL cubre el manejo estructurado de errores). Para auth, reenvía las credenciales explícitamente: construye el context del executor desde los headers de la petición entrante y pasa authorization a cada subesquema, que aplica sus propias reglas. El gateway es un pasarela, no una frontera de auth.
Una nota de versiones: la API antigua mergeSchemas de graphql-tools y el paquete schema-stitching están deprecados — stitchSchemas de @graphql-tools/stitch, el que se usa aquí, es la API actual y combina con Apollo Server 4.
Variantes
Delegación en lote
En lugar de resolver posts por usuario, agrupa la búsqueda:
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 en lote que recolecta todos los valores de authorId y hace una sola petición — el patrón de resolvers en lote cubre la solución general al N+1 en detalle.
Campos calculados
A veces el campo que quieres no existe en ningún subesquema — calcúlalo 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 prácticas
- Mantén los subesquemas pequeños y enfocados en un dominio.
- Usa
selectionSeten cada campo fusionado para evitar over-fetching. - Agrupa delegaciones en lote al resolver campos de lista para evitar llamadas N+1.
- Guarda en caché los resultados del executor cuando sea posible para reducir latencia del gateway.
- Documenta qué servicio posee qué tipo para evitar conflictos de merge.
- Monitoriza la latencia de delegación — el gateway añade un salto de red por delegación, así que trázalo como cualquier llamada downstream.
- Trata el esquema del gateway como una superficie de API real: versiona los tipos de enlace con cuidado, porque cada equipo que extiende un tipo ajeno se acopla al ciclo de releases de ese servicio.
Errores comunes
- Falta
selectionSet: la delegación no encuentra en el objeto padre los campos que necesita, y los campos fusionados vuelven null. - Dependencias circulares: el servicio A extiende el tipo del servicio B, el B extiende el del A — funciona, pero crea bucles infinitos de resolución si no se tiene cuidado.
- Campos de query raíz superpuestos: dos subesquemas que definen
Query.user(id: ID!)— configura reglas de merge o renombra uno. - No agrupar delegaciones de lista: resolver
postspara 50 usuarios uno por uno causa 50 peticiones al servicio de posts. - Ignorar la propagación de errores: los errores de los subesquemas necesitan mapeo adecuado a nivel de gateway — un fallo de subesquema sin mapear parece un bug de tu gateway para los clientes.
Lecturas adicionales
- Documentación de schema stitching de graphql-tools —
stitchSchemas, configuración de subesquemas, delegación y type merging. - Documentación de Apollo Federation — la alternativa a nivel de servicio cuando controlas todos los subgraphs.
- @graphql-tools/wrap — envuelve endpoints no-GraphQL (REST, gRPC) como subesquemas unibles.
- Código complementario: el repo companion de schema stitching tiene versiones ejecutables del gateway, ambos subesquemas y los resolvers de delegación de esta página.
Preguntas frecuentes
¿Cuál es la diferencia entre schema stitching y federation?
Federation (Apollo Federation) es el estándar actual para GraphQL distribuido: los servicios declaran cómo se componen sus tipos y un query planner decide la ejecución. Stitching fusiona las definiciones de tipos manualmente en el gateway — más simple de acoplar a APIs que no controlas, pero la configuración del merge, los resolvers de delegación y las reglas de conflicto son tuyas.
¿Está deprecado schema stitching?
La API antigua mergeSchemas de graphql-tools y el paquete independiente schema-stitching, sí. stitchSchemas de @graphql-tools/stitch — el que usa este patrón — se mantiene activamente. Para proyectos nuevos donde controlas todos los servicios, evalúa federation primero; stitching sigue siendo la opción práctica para esquemas de terceros o legacy.
¿Puedo unir APIs REST?
Sí. Envuelve endpoints REST como subesquemas GraphQL usando @graphql-tools/wrap y un executor personalizado que traduce operaciones GraphQL a llamadas HTTP.
¿Cómo maneja stitching la autenticación?
El gateway extrae los tokens de auth de las peticiones entrantes y los reenvía a los subesquemas vía el context del executor. Cada subesquema aplica sus propias reglas de autorización — el gateway pasa las credenciales, no toma decisiones de autorización.
¿Cómo afecta stitching al rendimiento?
Cada delegación añade un round-trip de red. Usa procesamiento en lote, caché y DataLoader a nivel de gateway para minimizar llamadas. Para escenarios de alto tráfico, considera federation con su query planning.
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.
PatternPatron de Extension de Errores en GraphQL
Adjunta metadatos estructurados a errores GraphQL usando codigos de extension para manejo de errores predecible del lado del cliente.
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.