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:
- Definir el tipo de entrada en el esquema GraphQL con tipos específicos
- Crear un resolver que reciba los argumentos validados
- Implementar lógica de validación antes de la operación de base de datos
- 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:
| Campo | Regla de validación | Mensaje de error |
|---|---|---|
| fecha | Debe ser futura (no pasada) | "La fecha debe ser futura" |
| personas | Entre 1 y 12 personas | "El número de personas debe estar entre 1 y 12" |
| telefono | Formato de teléfono válido | "Teléfono inválido" |
| Email 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
- Puedo definir un tipo de entrada (
input) en el esquema GraphQL con todos los campos necesarios - Sé implementar validación de formato (email, teléfono, URLs) en los resolvers
- Puedo validar reglas de negocio (rangos, unicidad, dependencias entre campos)
- Sé estructurar respuestas de error informativas para el cliente
- Puedo diferenciar entre errores de validación y errores del servidor
- Sé usar tipos escalares personalizados para validación a nivel de esquema
- 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.
- Crea un tipo de entrada
ActualizarPerfilInputen tu esquema GraphQL con estos campos:nombre(String, opcional)email(String, opcional)fechaNacimiento(String, opcional)biografia(String, opcional)
- Implementa el resolver
actualizarPerfilen 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)
- Estructura la respuesta usando un tipo
ActualizarPerfilResponseque incluya:success(Boolean)message(String)usuario(Usuario)errors([ValidationError])
- Implementa lógica para actualizar solo los campos proporcionados (no sobrescribir con null)
- Testea tu mutación con al menos 3 casos: datos válidos, datos inválidos, y mezcla de válidos/inválidos
- 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.