Saltar al contenido

Lección 6 de 20 · Lectura y práctica

Implementar Mutaciones con Validación de Datos

Concepto claveLas mutaciones en GraphQL son operaciones que modifican datos en el servidor, a diferencia de las consultas que solo los leen. En APIs profesionales, las mutaciones deben incluir validación de datos para garantizar.

Del concepto a la práctica

Taller de esta lección

Una mutación segura valida antes de cambiar el estado y debe distinguir entrada inválida de recurso inexistente. Trabajar con un input pequeño hace visible el orden: normalizar, comprobar reglas, localizar duplicados y solo entonces insertar.

Ejemplo resuelto

Para registrar una etiqueta:

graphql
input CreateTagInput { name: String!, color: String! }

Acepta name: "backend" y color: "#204060"; rechaza nombre vacío, color sin seis dígitos hexadecimales o nombre repetido. La colección inicial contiene frontend y graphql.

  1. Recorta espacios del nombre.

  2. Comprueba longitud y patrón del color.

  3. Busca coincidencia sin distinguir mayúsculas.

  4. Inserta solo después de superar todas las reglas.

Error frecuente: insertar primero y validar después, dejando un registro parcial. Corrección: ejecuta todas las comprobaciones antes de modificar la colección.

Pruébalo

Intenta crear GraphQL con color #123456 y luego graphql con #ffffff.

Comprueba tu resultado

Ambas entradas se rechazan como duplicadas porque graphql ya estaba en la colección inicial; el número de registros permanece en dos.

Cuenta las inserciones con el fixture real

Después prueba backend con #204060: supera la validación y aumenta la colección de 2 a 3. Repetir Backend con otra capitalización se rechaza y conserva 3. El input String! comprueba tipo y nulabilidad, pero permite una cadena vacía: el servicio debe imponer longitud y patrón del color. En una base compartida, aplica además una restricción única sobre el nombre normalizado; una lectura previa puede competir con otra inserción. Nunca dejes que un error posterior produzca una etiqueta incompleta.

Navegador, API y base de datos conectados por solicitudes, consultas y respuestas en sentidos opuestos.
CursaloMapa de práctica
Del navegador a los datos

El navegador envía una solicitud a la API. El servidor valida la operación y consulta los datos; después devuelve una respuesta que el navegador puede mostrar. La base de datos se comunica con el servidor y no directamente con el navegador.

Ver la infografía en tamaño completo ↗
Fuentes para profundizar

Concepto clave

Las mutaciones en GraphQL son operaciones que modifican datos en el servidor, a diferencia de las consultas que solo los leen. En APIs profesionales, las mutaciones deben incluir validación de datos para garantizar que la información recibida cumple con las reglas de negocio antes de procesarla. Esto es similar a un formulario de registro en línea: el sistema verifica que el correo electrónico tenga formato válido y la contraseña cumpla requisitos de seguridad antes de crear la cuenta.

En el contexto de GraphQL con TypeScript y Apollo Server, la validación se implementa típicamente en dos capas: primero en el esquema GraphQL usando tipos escalares personalizados y directivas, y luego en los resolvers mediante lógica de validación explícita. Esta doble capa protege contra datos malformados y ataques comunes como inyección de datos.

Cómo funciona en la práctica

Implementar una mutación con validación sigue estos pasos:

  1. Definir el tipo de entrada en el esquema GraphQL con tipos específicos
  2. Crear un resolver que reciba los argumentos validados
  3. Implementar lógica de validación antes de la operación de base de datos
  4. Devolver un tipo de respuesta estandarizado que incluya posibles errores

Ejemplo básico de esquema:

type Mutation {
  crearUsuario(input: CrearUsuarioInput!): UsuarioResponse!
}

input CrearUsuarioInput {
  email: String!
  password: String!
  edad: Int!
}

type UsuarioResponse {
  success: Boolean!
  message: String
  usuario: Usuario
  errors: [ValidationError!]
}

type ValidationError {
  field: String!
  message: String!
}

En el resolver, validamos los datos:

const crearUsuario = async (_, { input }) => {
  const errors = []
  
  // Validación de email
  if (!isValidEmail(input.email)) {
    errors.push({ field: 'email', message: 'Email inválido' })
  }
  
  // Validación de contraseña
  if (input.password.length < 8) {
    errors.push({ field: 'password', message: 'La contraseña debe tener al menos 8 caracteres' })
  }
  
  // Validación de edad
  if (input.edad < 18) {
    errors.push({ field: 'edad', message: 'Debes ser mayor de 18 años' })
  }
  
  if (errors.length > 0) {
    return {
      success: false,
      message: 'Errores de validación',
      usuario: null,
      errors
    }
  }
  
  // Si pasa validación, crear usuario
  const usuario = await db.usuarios.create(input)
  return {
    success: true,
    message: 'Usuario creado exitosamente',
    usuario,
    errors: []
  }
}

Caso de estudio

Imagina que estás construyendo un sistema de reservas para un restaurante. Necesitas una mutación crearReserva que valide:

CampoRegla de validaciónMensaje de error
fechaDebe ser futura (no pasada)"La fecha debe ser futura"
personasEntre 1 y 12 personas"El número de personas debe estar entre 1 y 12"
telefonoFormato de teléfono válido"Teléfono inválido"
emailEmail válido y único en el sistema"Email inválido o ya registrado"

Implementación del resolver:

const crearReserva = async (_, { input }, context) => {
  const { fecha, personas, telefono, email } = input
  const errors = []
  
  // Validar fecha futura
  if (new Date(fecha) < new Date()) {
    errors.push({ field: 'fecha', message: 'La fecha debe ser futura' })
  }
  
  // Validar número de personas
  if (personas < 1 || personas > 12) {
    errors.push({ field: 'personas', message: 'El número de personas debe estar entre 1 y 12' })
  }
  
  // Validar teléfono
  if (!/^[+]?[\d\s-]{10,}$/.test(telefono)) {
    errors.push({ field: 'telefono', message: 'Teléfono inválido' })
  }
  
  // Validar email único
  const existeEmail = await db.reservas.findUnique({ where: { email } })
  if (existeEmail) {
    errors.push({ field: 'email', message: 'Email ya registrado para otra reserva' })
  }
  
  if (errors.length > 0) {
    return {
      success: false,
      reserva: null,
      errors
    }
  }
  
  // Crear reserva
  const reserva = await db.reservas.create({
    data: { fecha, personas, telefono, email }
  })
  
  return {
    success: true,
    reserva,
    errors: []
  }
}
La validación en el servidor es crucial: nunca confíes en la validación del cliente, ya que puede ser omitida o manipulada.

Errores comunes

  • Validar solo en el cliente: Los atacantes pueden enviar peticiones directamente al servidor omitiendo la validación del frontend. Siempre valida en el servidor.
  • Mensajes de error genéricos: Evita mensajes como "Error en los datos". Sé específico sobre qué campo falló y por qué.
  • No validar unicidad: Olvidar verificar si un valor único (como email) ya existe en la base de datos.
  • Validar después de operaciones costosas: Realiza validaciones antes de cualquier operación de base de datos o cálculo complejo.
  • Falta de validación de tipos: Aunque GraphQL valida tipos básicos, necesitas validar reglas de negocio específicas.

Checklist de dominio

  1. Puedo definir un tipo de entrada (input) en el esquema GraphQL con todos los campos necesarios
  2. Sé implementar validación de formato (email, teléfono, URLs) en los resolvers
  3. Puedo validar reglas de negocio (rangos, unicidad, dependencias entre campos)
  4. Sé estructurar respuestas de error informativas para el cliente
  5. Puedo diferenciar entre errores de validación y errores del servidor
  6. Sé usar tipos escalares personalizados para validación a nivel de esquema
  7. Puedo testear mutaciones con datos válidos e inválidos

Implementar mutación para actualizar perfil de usuario con validación

En este ejercicio, implementarás una mutación actualizarPerfil que permita a los usuarios actualizar su información personal con validación robusta.

  1. Crea un tipo de entrada ActualizarPerfilInput en tu esquema GraphQL con estos campos:
    • nombre (String, opcional)
    • email (String, opcional)
    • fechaNacimiento (String, opcional)
    • biografia (String, opcional)
  2. Implementa el resolver actualizarPerfil en Apollo Server con estas validaciones:
    • nombre: mínimo 2 caracteres, máximo 50 (si se proporciona)
    • email: formato válido y único en el sistema (si se proporciona)
    • fechaNacimiento: fecha válida y el usuario debe tener al menos 13 años (si se proporciona)
    • biografia: máximo 500 caracteres (si se proporciona)
  3. Estructura la respuesta usando un tipo ActualizarPerfilResponse que incluya:
    • success (Boolean)
    • message (String)
    • usuario (Usuario)
    • errors ([ValidationError])
  4. Implementa lógica para actualizar solo los campos proporcionados (no sobrescribir con null)
  5. Testea tu mutación con al menos 3 casos: datos válidos, datos inválidos, y mezcla de válidos/inválidos
Pistas
  • Usa operadores de propagación (...) para actualizar solo los campos proporcionados sin afectar los demás
  • Para validar la edad mínima, calcula la diferencia entre la fecha actual y fechaNacimiento
  • Considera usar una librería como validator.js para validaciones de formato complejas

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.

Sigue con GraphQL Profesional con Next.js y Apollo Server: APIs Tipadas y Suscripciones

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.