En el mundo real, igual que en programación, nadie está a salvo de errores. Los clientes pueden enviar datos incorrectos, la base de datos puede comportarse de forma extraña, y el desarrollador — bueno, es humano, puede olvidar algo importante. Por eso la validación y el manejo de errores son aspectos críticos de cualquier API.
GraphQL está diseñado para minimizar la cantidad de errores gracias a la tipificación estricta y la validación del esquema. Sin embargo, eso no es suficiente, especialmente cuando trabajamos con peticiones de usuario. Necesitamos herramientas para:
- Comprobar los datos de entrada según la lógica del negocio.
- Formar mensajes de error informativos.
- Localizar (i18n) los errores.
- Loguear los problemas para análisis posteriores.
Vamos a ver cómo se hace esto en el contexto de GraphQL con Spring.
Validación integrada en GraphQL
GraphQL, en su nivel más básico, usa el esquema para validar automáticamente las peticiones. Por ejemplo, si en el esquema se indica el tipo Int, GraphQL rechazará automáticamente una cadena enviada para ese campo.
Ejemplo de esquema con validación de tipos:
type Query {
getUserById(id: Int!): User
}
type User {
id: Int!
name: String!
email: String
}
En este ejemplo el campo id debe ser un número entero, y es obligatorio. GraphQL lo valida antes de ejecutar la petición. Sin embargo, en escenarios más complejos necesitamos herramientas adicionales.
Validación personalizada de datos de entrada
A veces la comprobación básica de tipos no es suficiente. Por ejemplo, quieres asegurarte de que el ID sea positivo, que el nombre no sea demasiado largo, o que el email tenga el formato correcto. Para estos casos Spring ofrece un conjunto amplio de herramientas de validación.
Uso de la anotación @Valid
Podemos usar javax.validation para validar los datos de entrada. Veamos un ejemplo:
1. Modificamos el esquema:
type Mutation {
createUser(input: CreateUserInput!): User
}
input CreateUserInput {
name: String!
email: String!
age: Int
}
type User {
id: Int!
name: String!
email: String
age: Int
}
2. Crearemos un DTO para procesar los datos de entrada:
import jakarta.validation.constraints.*;
public class CreateUserInput {
@NotBlank(message = "El nombre no puede estar vacío")
@Size(max = 50, message = "El nombre no puede tener más de 50 caracteres")
private String name;
@NotBlank(message = "El email es obligatorio")
@Email(message = "Formato de email incorrecto")
private String email;
@Min(value = 18, message = "La edad debe ser al menos 18")
@Max(value = 120, message = "La edad no puede ser mayor de 120")
private Integer age;
// Getters y setters
}
3. Actualizamos el resolver para manejar la mutación:
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
@Component
@Validated
public class UserMutationResolver {
public User createUser(@Valid CreateUserInput input) {
// Lógica para crear el usuario
User user = new User();
user.setId(1); // Mockeamos el ID
user.setName(input.getName());
user.setEmail(input.getEmail());
user.setAge(input.getAge());
return user;
}
}
4. Manejar las excepciones: Si el usuario envía datos incorrectos, Spring generará automáticamente la excepción MethodArgumentNotValidException. Necesitamos capturarla y devolver un mensaje claro al cliente.
Manejo de errores:
import graphql.GraphQLError;
import graphql.kickstart.execution.error.GraphQLErrorHandler;
import graphql.language.SourceLocation;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class CustomGraphQLErrorHandler implements GraphQLErrorHandler {
@Override
public List<GraphQLError> processErrors(List<GraphQLError> errors) {
return errors.stream()
.map(error -> {
if (error.getMessage().contains("Validation failed")) {
return GraphQLError.newError()
.message("Validación fallida: " + error.getMessage())
.location(new SourceLocation(error.getLocations().get(0).getLine(), error.getLocations().get(0).getColumn()))
.build();
}
return error;
}).toList();
}
}
Manejo de errores en GraphQL
En caso de cualquier error, GraphQL devuelve una estructura que contiene tanto los datos como un array de errores. Por ejemplo:
{
"data": null,
"errors": [
{
"message": "Validación fallida: El nombre no puede estar vacío",
"locations": [
{
"line": 2,
"column": 3
}
],
"extensions": {
"code": "VALIDATION_ERROR"
}
}
]
}
Ejemplo de creación de errores personalizados con GraphQLError
Podemos crear nuestros propios errores para tener un control más preciso:
import graphql.GraphQLError;
import graphql.language.SourceLocation;
import java.util.List;
public class ValidationError implements GraphQLError {
private final String message;
public ValidationError(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
@Override
public List<SourceLocation> getLocations() {
return null; // La ubicación se puede dejar null si no es crítica
}
@Override
public Map<String, Object> getExtensions() {
return Map.of("code", "VALIDATION_ERROR");
}
}
Para no capturar cada error manualmente, podemos definir un manejador tipo @ControllerAdvice:
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
import org.springframework.stereotype.Component;
@Component
public class GlobalExceptionHandler extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex) {
if (ex instanceof IllegalArgumentException) {
return new ValidationError(ex.getMessage());
}
return null;
}
}
Localización de mensajes de error
Para aplicaciones internacionales es importante ofrecer los mensajes de error en el idioma del usuario. Spring tiene soporte integrado para i18n mediante archivos messages.properties.
Configuración de localización:
1. Crea archivos property:
messages.properties(por defecto)messages_ru.properties(para ruso)
2. Ejemplo del contenido del archivo:
name.notBlank=El nombre no puede estar vacío
email.invalid=Formato de email incorrecto
3. Configura el LocaleResolver:
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
@Configuration
public class LocaleConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasename("messages");
source.setDefaultEncoding("UTF-8");
return source;
}
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.ENGLISH);
return resolver;
}
}
Registro (logging) de errores
No solo es importante manejar los errores para los clientes, sino también registrarlos para análisis posteriores. Usa librerías como SLF4J y Logback:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class LoggingErrorHandler {
private static final Logger logger = LoggerFactory.getLogger(LoggingErrorHandler.class);
public void logError(Throwable ex) {
logger.error("Ha ocurrido un error: {}", ex.getMessage(), ex);
}
}
Estos enfoques asegurarán una validación de datos y un manejo de errores de calidad en tu API GraphQL, haciéndola más fiable y cómoda para interactuar por parte de clientes y desarrolladores.
GO TO FULL VERSION