GraphQL y CQRS
Capítulo 20: GraphQL y CQRS
GraphQL es un lenguaje de consulta para APIs desarrollado por Facebook. A diferencia de REST que usa múltiples endpoints, GraphQL expone un único endpoint donde el cliente especifica exactamente qué datos necesita. Su estructura se alinea naturalmente con CQRS.
Por qué GraphQL con CQRS
GraphQL tiene dos tipos de operaciones principales que mapean perfectamente a CQRS:
- Query: Operaciones de lectura (Read Side)
- Mutation: Operaciones de escritura (Write Side)
Además, Subscriptions permiten notificaciones en tiempo real, ideales para propagar eventos del sistema.
Schema GraphQL
El schema define el contrato de la API: qué datos se pueden consultar, qué mutaciones existen, y la estructura de cada tipo.
- type Query: Define todas las consultas disponibles (Read Side)
- type Mutation: Define todas las operaciones de escritura (Write Side)
- type Order: Define la estructura de un pedido
- input: Define estructuras de entrada para mutations
- enum: Define valores posibles para campos como OrderStatus
# src/graphql/schema.graphql
type Query {
order(id: ID!): Order
orders(customerId: ID!, page: Int = 0, limit: Int = 20): OrderList!
searchOrders(filter: OrderFilter!): [Order!]!
}
type Mutation {
createOrder(input: CreateOrderInput!): CreateOrderPayload!
addItem(input: AddItemInput!): AddItemPayload!
confirmOrder(orderId: ID!): ConfirmOrderPayload!
}
type Order {
id: ID!
customerId: ID!
items: [OrderItem!]!
status: OrderStatus!
total: Float!
createdAt: DateTime!
}
type OrderItem {
productId: ID!
name: String!
price: Float!
quantity: Int!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
}
type OrderList {
orders: [Order!]!
total: Int!
page: Int!
hasMore: Boolean!
}
input CreateOrderInput {
customerId: ID!
}
input AddItemInput {
orderId: ID!
productId: ID!
name: String!
price: Float!
quantity: Int!
}
input OrderFilter {
status: OrderStatus
minTotal: Float
maxTotal: Float
fromDate: DateTime
toDate: DateTime
}
type CreateOrderPayload {
orderId: ID!
success: Boolean!
}
type AddItemPayload {
success: Boolean!
}
type ConfirmOrderPayload {
success: Boolean!
}
Resolvers de Queries
Los resolvers son funciones que se ejecutan cuando el cliente solicita un campo. Conectan el schema GraphQL con la lógica de negocio (en nuestro caso, el Query Bus de CQRS).
El primer parámetro _ es el objeto padre (no usado en queries raíz). El segundo contiene los argumentos del cliente.
// src/graphql/resolvers/queries.ts
import { QueryBus } from '../../query/bus';
import { GetOrderQuery } from '../../query/orders/get-order';
import { ListOrdersQuery } from '../../query/orders/list-orders';
import { SearchOrdersQuery } from '../../query/orders/search-orders';
export const createQueryResolvers = (queryBus: QueryBus) => ({
Query: {
order: async (_: unknown, { id }: { id: string }) => {
const query = new GetOrderQuery(id);
return queryBus.ask(query);
},
orders: async (_: unknown, args: { customerId: string; page: number; limit: number }) => {
const query = new ListOrdersQuery(args.customerId, args.page, args.limit);
return queryBus.ask(query);
},
searchOrders: async (_: unknown, { filter }: { filter: OrderFilter }) => {
const query = new SearchOrdersQuery(filter);
return queryBus.ask(query);
}
}
});
Resolvers de Mutations
Los mutations se conectan al Command Bus. Cada mutation crea un comando, lo despacha, y retorna un payload indicando el resultado.
Los payloads de mutation usan el patrón success: boolean para indicar si la operación fue exitosa. Alternativamente se pueden usar union types para representar éxito o diferentes tipos de errores.
// src/graphql/resolvers/mutations.ts
import { v4 as uuid } from 'uuid';
import { CommandBus } from '../../command/bus';
import { CreateOrderCommand } from '../../command/orders/create-order';
import { AddItemCommand } from '../../command/orders/add-item';
import { ConfirmOrderCommand } from '../../command/orders/confirm-order';
export const createMutationResolvers = (commandBus: CommandBus) => ({
Mutation: {
createOrder: async (_: unknown, { input }: { input: CreateOrderInput }) => {
const orderId = uuid();
const command = new CreateOrderCommand(orderId, input.customerId);
try {
await commandBus.dispatch(command);
return { orderId, success: true };
} catch (error) {
return { orderId: null, success: false };
}
},
addItem: async (_: unknown, { input }: { input: AddItemInput }) => {
const command = new AddItemCommand(
input.orderId,
input.productId,
input.name,
input.price,
input.quantity
);
try {
await commandBus.dispatch(command);
return { success: true };
} catch {
return { success: false };
}
},
confirmOrder: async (_: unknown, { orderId }: { orderId: string }) => {
const command = new ConfirmOrderCommand(orderId);
try {
await commandBus.dispatch(command);
return { success: true };
} catch {
return { success: false };
}
}
}
});
Configuración Apollo Server
Apollo Server es la implementación más popular de servidor GraphQL para Node.js. expressMiddleware integra Apollo con Express.
readFileSync carga el schema desde un archivo .graphql, manteniendo separado el schema de la implementación.
// src/graphql/server.ts
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { readFileSync } from 'fs';
import { createQueryResolvers } from './resolvers/queries';
import { createMutationResolvers } from './resolvers/mutations';
export const createGraphQLServer = async (commandBus: CommandBus, queryBus: QueryBus) => {
const typeDefs = readFileSync('./src/graphql/schema.graphql', 'utf-8');
const resolvers = {
...createQueryResolvers(queryBus),
...createMutationResolvers(commandBus)
};
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
return expressMiddleware(server);
};
Subscriptions para Eventos
Las Subscriptions permiten que el cliente reciba actualizaciones en tiempo real via WebSocket. Son ideales para notificar cambios de estado generados por eventos CQRS.
PubSub es un sistema de publicación/suscripción en memoria. En producción se usa Redis PubSub o similar para escalar a múltiples instancias.
asyncIterator crea un iterador que emite valores cada vez que se publica en el topic especificado.
# Agregar al schema
type Subscription {
orderStatusChanged(orderId: ID!): OrderStatusUpdate!
}
type OrderStatusUpdate {
orderId: ID!
previousStatus: OrderStatus!
newStatus: OrderStatus!
changedAt: DateTime!
}
// src/graphql/resolvers/subscriptions.ts
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
export const subscriptionResolvers = {
Subscription: {
orderStatusChanged: {
subscribe: (_: unknown, { orderId }: { orderId: string }) => {
return pubsub.asyncIterator([`ORDER_STATUS_${orderId}`]);
}
}
}
};
// Publicar desde proyección
export const publishStatusChange = (update: OrderStatusUpdate) => {
pubsub.publish(`ORDER_STATUS_${update.orderId}`, {
orderStatusChanged: update
});
};
Ejemplos de Uso
GraphQL permite al cliente especificar exactamente qué campos necesita. Esto evita over-fetching (traer más datos de los necesarios) y under-fetching (necesitar múltiples requests).
# Crear pedido
mutation {
createOrder(input: { customerId: "cust-123" }) {
orderId
success
}
}
# Consultar pedido con items
query {
order(id: "order-456") {
id
status
total
items {
name
quantity
price
}
}
}
# Buscar pedidos
query {
searchOrders(filter: { status: PENDING, minTotal: 100 }) {
id
total
createdAt
}
}
Resumen
GraphQL con CQRS:
- Mutations mapean directamente a comandos
- Queries aprovechan el read model optimizado
- Subscriptions notifican cambios en tiempo real
- Schema documenta la API automáticamente
Glosario
GraphQL
Definición: Lenguaje de consulta para APIs que permite al cliente especificar exactamente qué datos necesita. Desarrollado por Facebook, usa un único endpoint en lugar de múltiples como REST.
Por qué es importante: Elimina over-fetching y under-fetching. El cliente controla la forma de la respuesta, reduciendo transferencia de datos y número de requests.
Ejemplo práctico: En lugar de GET /orders/123 que retorna todos los campos, el cliente puede pedir query { order(id: "123") { id, total } } obteniendo solo id y total.
Schema
Definición: Definición del contrato de una API GraphQL. Especifica tipos disponibles, queries, mutations, y la estructura de cada tipo de dato.
Por qué es importante: El schema es la fuente de verdad. Clientes y servidor deben cumplirlo. Herramientas generan tipos, documentación y validación automáticamente.
Ejemplo práctico: type Order { id: ID!, total: Float! } define que Order tiene un id obligatorio de tipo ID y un total obligatorio de tipo Float.
Resolver
Definición: Función que se ejecuta cuando el cliente solicita un campo en el schema. Conecta el schema GraphQL con la lógica de negocio o fuentes de datos.
Por qué es importante: Los resolvers implementan la lógica real. El schema define qué se puede pedir, los resolvers definen cómo se obtiene.
Ejemplo práctico: El resolver de order(id) recibe el id, crea un GetOrderQuery, lo envía al QueryBus, y retorna el resultado.
Mutation
Definición: Operación GraphQL que modifica datos en el servidor. Equivalente a POST/PUT/DELETE en REST.
Por qué es importante: Separa claramente operaciones de escritura de lecturas. En CQRS, mutations se mapean directamente a comandos.
Ejemplo práctico: mutation { createOrder(input: {...}) { orderId } } crea un pedido y retorna el ID generado.
Subscription
Definición: Operación GraphQL que mantiene una conexión abierta (típicamente WebSocket) para recibir actualizaciones en tiempo real.
Por qué es importante: Permite notificaciones push sin polling. Ideal para dashboards, chats, y notificaciones de cambios de estado.
Ejemplo práctico: subscription { orderStatusChanged(orderId: "123") { newStatus } } notifica automáticamente cuando el pedido cambia de estado.
Apollo Server
Definición: Implementación popular de servidor GraphQL para Node.js. Soporta queries, mutations, subscriptions, y se integra con Express, Fastify, etc.
Por qué es importante: Proporciona una implementación robusta y bien mantenida con excelente documentación y ecosistema de herramientas.
Ejemplo práctico: new ApolloServer({ typeDefs, resolvers }) crea un servidor que procesa queries GraphQL basándose en el schema y resolvers.
PubSub
Definición: Sistema de publicación/suscripción que permite a productores publicar eventos y consumidores suscribirse a ellos.
Por qué es importante: Desacopla quién genera eventos de quién los consume. Esencial para implementar subscriptions GraphQL.
Ejemplo práctico: pubsub.publish('ORDER_STATUS_123', data) envía data a todos los clientes suscritos al topic ‘ORDER_STATUS_123’.
Input Type
Definición: Tipo especial en GraphQL usado exclusivamente como argumento de mutations y queries. Definido con la palabra clave input en lugar de type.
Por qué es importante: Separa claramente tipos de entrada de tipos de salida. Los input types no pueden tener campos que sean otros types regulares.
Ejemplo práctico: input CreateOrderInput { customerId: ID! } define la estructura de entrada para la mutation createOrder.
Payload
Definición: Tipo de retorno de una mutation que indica el resultado de la operación. Típicamente incluye el recurso creado/modificado y/o un indicador de éxito.
Por qué es importante: Proporciona información estructurada sobre el resultado. Permite retornar tanto éxito como errores de forma tipada.
Ejemplo práctico: CreateOrderPayload { orderId: ID, success: Boolean! } retorna el ID del pedido creado y si la operación fue exitosa.