CodeGym /Courses /Module 5. Spring /Lecture 289: Validation and Error Handling in GraphQL

Lecture 289: Validation and Error Handling in GraphQL

Module 5. Spring
Level 15 , Lesson 8
Available

In the real world, just like in programming, nobody's immune to mistakes. Clients can send bad data, the database can act weird, and a developer — well, they're human and can forget something important. That's why validation and error handling are critical aspects of any API.

GraphQL is designed to reduce the number of errors via strict typing and schema validation. But that's not enough, especially when dealing with custom business rules. We need tools to:

  1. Validate input data against business logic.
  2. Produce informative error messages.
  3. Localize errors.
  4. Log issues for further analysis.

Let's see how this is done in the context of GraphQL with Spring.


Built-in validation in GraphQL

At the most basic level, GraphQL uses the schema to automatically validate requests. For example, if the schema specifies type Int, GraphQL will reject a string sent for that field.

Example schema with type validation:


type Query {
    getUserById(id: Int!): User
}

type User {
    id: Int!
    name: String!
    email: String
}

In this example, the id field must be an integer and is required. GraphQL checks this before executing the request. However, for more complex scenarios we need additional tools.


Custom input validation

Sometimes basic type checking isn't enough. For example, you might want to ensure an ID is positive, a name isn't too long, or an email is in the correct format. For these cases, Spring provides a rich validation toolbox.

Using the @Valid annotation

We can use javax.validation to validate input data. Here's an example:

1. Update the schema:


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. Create a DTO to handle input data:


import jakarta.validation.constraints.*;

public class CreateUserInput {
    @NotBlank(message = "Name cannot be blank")
    @Size(max = 50, message = "Name cannot be longer than 50 characters")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    private String email;

    @Min(value = 18, message = "Age must be at least 18")
    @Max(value = 120, message = "Age cannot be greater than 120")
    private Integer age;

    // Getters and setters
}

3. Update the resolver to handle the mutation:


import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

@Component
@Validated
public class UserMutationResolver {

    public User createUser(@Valid CreateUserInput input) {
        // User creation logic
        User user = new User();
        user.setId(1); // Mocking the ID
        user.setName(input.getName());
        user.setEmail(input.getEmail());
        user.setAge(input.getAge());
        return user;
    }
}

4. Handle exceptions: If a client sends invalid data, Spring will automatically throw a MethodArgumentNotValidException. We need to catch it and return a friendly message to the client.

Error handling:


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("Validation failed: " + error.getMessage())
                                .location(new SourceLocation(error.getLocations().get(0).getLine(), error.getLocations().get(0).getColumn()))
                                .build();
                    }
                    return error;
                }).toList();
    }
}

Error handling in GraphQL

On any error, GraphQL returns a structure that contains both data (if any) and an array of errors. For example:


{
  "data": null,
  "errors": [
    {
      "message": "Validation failed: Name cannot be blank",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "extensions": {
        "code": "VALIDATION_ERROR"
      }
    }
  ]
}

Example of creating custom errors with GraphQLError

We can create our own errors for finer control:


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; // Location can be left null if it's not critical
    }

    @Override
    public Map<String, Object> getExtensions() {
        return Map.of("code", "VALIDATION_ERROR");
    }
}

To avoid catching every error manually, we can define a handler like @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;
    }
}

Localizing error messages

For international apps it's important to provide error messages in the user's language. Spring has built-in localization support via messages.properties files.

Localization setup:

1. Create property files:

  • messages.properties (default)
  • messages_ru.properties (for Russian)

2. Example file contents:


name.notBlank=Name cannot be blank
email.invalid=Invalid email format

3. Configure the 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;
    }
}

Logging errors

It's important not only to handle errors for clients but also to log them for later analysis. Use libraries like SLF4J and 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("An error occurred: {}", ex.getMessage(), ex);
    }
}

These approaches will give you solid data validation and error handling in your GraphQL API, making it more reliable and easier to work with for both clients and developers.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION