Quiz: Resolvers y Mutaciones en GraphQL

Quiz
10 min~5 min lectura

Quiz Interactivo

Pon a prueba tus conocimientos

Concepto clave

Los resolvers en GraphQL son funciones que se encargan de obtener los datos para cada campo definido en tu esquema. Piensa en ellos como los "traductores" entre tu API GraphQL y tus fuentes de datos (bases de datos, servicios externos, etc.). Cada resolver recibe cuatro argumentos: parent, args, context y info, que te permiten acceder a datos del nivel superior, argumentos de la consulta, información compartida (como autenticación) y metadatos de la consulta.

Las mutaciones son operaciones que modifican datos en el servidor. A diferencia de las queries (que son de solo lectura), las mutaciones siguen el principio de "escritura seguida de lectura": primero ejecutan el cambio y luego devuelven el resultado actualizado. En un contexto profesional, las mutaciones deben ser idempotentes cuando sea posible (ejecutarlas múltiples veces produce el mismo resultado) y manejar adecuadamente la concurrencia.

"Un resolver bien diseñado es como un buen camarero: sabe exactamente qué pedido traer (args), de qué mesa viene (parent), conoce al cliente (context) y puede sugerir extras si es necesario (info)."

Cómo funciona en la práctica

Imagina que estás construyendo una API para un sistema de tareas (todo list). Tu esquema GraphQL podría definir tipos como Task y User, con mutaciones para crear y actualizar tareas. Aquí un ejemplo paso a paso de un resolver para una mutación:

// Definición en el esquema
type Mutation {
  createTask(title: String!, description: String): Task!
}

// Resolver en Apollo Server
const resolvers = {
  Mutation: {
    createTask: async (parent, args, context, info) => {
      // 1. Validar los argumentos
      if (!args.title.trim()) {
        throw new Error('El título es requerido');
      }
      
      // 2. Usar el contexto para autenticación
      if (!context.user) {
        throw new Error('No autorizado');
      }
      
      // 3. Lógica de negocio: crear la tarea en la base de datos
      const newTask = await prisma.task.create({
        data: {
          title: args.title,
          description: args.description || '',
          userId: context.user.id,
          createdAt: new Date()
        }
      });
      
      // 4. Retornar el resultado tipado
      return {
        id: newTask.id,
        title: newTask.title,
        description: newTask.description,
        completed: false
      };
    }
  }
};

Este resolver sigue un flujo típico: validación, autorización, operación de datos y retorno tipado. Nota cómo usa context.user para asegurar que solo usuarios autenticados puedan crear tareas.

Caso de estudio

Considera una plataforma de e-commerce con GraphQL. Necesitas una mutación para que los usuarios agreguen productos al carrito. Los requisitos son:

  • El usuario debe estar autenticado
  • El producto debe existir y tener stock disponible
  • Si el producto ya está en el carrito, incrementar la cantidad
  • Retornar el carrito actualizado con el total calculado

Implementación clave:

addToCart: async (_, { productId, quantity }, { user, dataSources }) => {
  // Verificar autenticación
  if (!user) throw new AuthenticationError('Debes iniciar sesión');
  
  // Validar producto y stock
  const product = await dataSources.inventoryAPI.getProduct(productId);
  if (!product || product.stock < quantity) {
    throw new UserInputError('Producto no disponible');
  }
  
  // Lógica de negocio: agregar o actualizar en carrito
  const cart = await dataSources.cartService.addItem({
    userId: user.id,
    productId,
    quantity,
    price: product.price
  });
  
  // Retornar tipo Cart con relaciones resueltas
  return {
    id: cart.id,
    items: cart.items,
    total: cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  };
}

Este caso muestra cómo los resolvers integran múltiples servicios (inventario, carrito) y manejan errores específicos de GraphQL como AuthenticationError.

Errores comunes

  1. Resolvers bloqueantes: Usar operaciones síncronas pesadas que bloquean el event loop. Solución: Siempre usar async/await o Promises para operaciones I/O.
  2. Validación insuficiente: Confiar solo en la validación del cliente. Solución: Validar todos los inputs en el resolver, usando librerías como Zod o Joi.
  3. Contexto mal utilizado: Poner datos volátiles en el contexto que deberían ir en args. Solución: Usar context solo para datos compartidos como autenticación, no para inputs de operaciones.
  4. Manejo pobre de errores: Lanzar errores genéricos. Solución: Usar errores específicos de GraphQL (AuthenticationError, UserInputError) para mejor experiencia cliente.
  5. N+1 queries: Hacer consultas separadas por cada item en una lista. Solución: Implementar batching o usar DataLoader para optimizar.

Checklist de dominio

  • ✓ Puedo explicar la diferencia entre queries y mutaciones en términos de idempotencia
  • ✓ Implemento validación de inputs tanto en esquema como en resolvers
  • ✓ Uso el contexto adecuadamente para autenticación y servicios compartidos
  • ✓ Manejo errores específicos de GraphQL en lugar de errores genéricos
  • ✓ Optimizo resolvers para evitar problemas N+1 con DataLoader
  • ✓ Escribo mutaciones que son seguras para ejecución concurrente
  • ✓ Documento mis resolvers con comentarios que explican decisiones de negocio

Implementa una mutación para actualizar perfil de usuario

En este ejercicio, crearás una mutación GraphQL tipada para actualizar el perfil de un usuario en un sistema real. Sigue estos pasos:

  1. Define el tipo de entrada: Crea un input type UpdateProfileInput con campos: name (String, opcional), email (String, opcional con validación de email), bio (String, opcional, máximo 500 caracteres).
  2. Define la mutación en el esquema: Agrega updateProfile(input: UpdateProfileInput!): User! a tu tipo Mutation.
  3. Implementa el resolver: En tu archivo de resolvers, crea la función updateProfile que:
    • Verifica que el usuario esté autenticado (context.user)
    • Valida que al menos un campo sea proporcionado
    • Actualiza solo los campos proporcionados en la base de datos
    • Retorna el objeto User actualizado con todos sus campos
  4. Agrega manejo de errores: Incluye validación para email único y maneja errores con UserInputError.
  5. Prueba con una consulta: Ejecuta una mutación de ejemplo que actualice solo el nombre.
Pistas
  • Usa el operador spread (...) para actualizar solo los campos proporcionados sin sobrescribir otros.
  • Considera usar una librería como 'validator' para validar el formato del email en el resolver.
  • Recuerda que en GraphQL, los campos opcionales en el input pueden ser null, pero debes diferenciar entre null y undefined.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.