Implementar Mutaciones con Validación de Datos

Lectura
20 min~5 min lectura

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.