← Volver al listado de tecnologías

GraphQL y CQRS

Por: SiempreListo
cqrsgraphqlapitypescriptapollo

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:

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.

# 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:

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.


← Capítulo 19: API REST | Capítulo 21: Frontend React →