Graphql Mutation and error handling - complete tutorial

Graphql Mutation and error handling in spring boot 

In the previous Create GraphQL using Spring Boot, we have learned how to fetch data from graphQL server using graplQL query but in a real-time mobile app or any project not only need data from server but also need some function to update data.

In today's blog, we are going to learn how to update, create and delete data on the graphql server.

Integrating GraphQL in Spring boot API

GraphQL Mutation

In GraphQL, mutations are used to perform some manipulation operations in server data such as updating, creation, and deleting operations.

*
type Mutation {
	addEmployee(employee: EmployeeInput): Employee
	updateEmployee(employee: EmployeeInput): Employee
}


In the above, the addEmployee and updateEmployee are mutations defined in the schema.

The addEmployee and updateEmployee take EmployeeInput object as input and respond back Employee object.

The main difference between query and mutation is that query operations are executed in parallel where mutations are executed in series one after the other.

GraphQL input type

In graphQL, the input type is used to define input arguments to mutation operations. they are similar to graphQL object types except used as an argument.

The input keyword is used to define input arguments.

input EmployeeInput {
  id: Int	
  name: String
  jobTitle: String
  mobileNo: String
  joinedDate: String
  gender: String
  salary: Int
  department: DepartmentInput
  project: ProjectInput
}

input DepartmentInput {
  id: ID
  name: String
}

input ProjectInput {
  id: ID
  name: String
}

If you see the above schema we have defined EmployeeInput as input which is going to be used as an argument in addEmployee and updateEmployee operations.

Now the client query looks like below. The $employee is a query variable that the client pass to the query function. 

mutation($employee:EmployeeInput){
  updateEmployee(employee:$employee){
    id
    name
    department{
      id
      name
    }
    project{
      id
      name
    }
  }
}

same like query the mutation operation also returns an Employee object type and we are asking for id, name, and department details from the employee object.

Create GrpahQl mutation resolver

We will create a class and implement GraphQLMutationResolver as below.

we have defined two methods addEmloyee and updateEmployee to add/update employee details in the database.


import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.example.graphql.exception.InvalidEmployeeException;
import com.example.graphql.model.Employee;
import com.example.graphql.repo.EmployeeRepository;

@Component
public class GraphQLMutationService implements GraphQLMutationResolver {

	@Autowired
	EmployeeRepository employeeRepository;

	public Employee addEmployee(Employee employee) {
		System.out.println("Employee input " + employee);
		return employeeRepository.save(employee);
	}

	public Employee updateEmployee(Employee employee) {

		System.out.println(employee);
		Employee emp = null;
		if (Objects.nonNull(employee)) {
			boolean isEmployeePresent = employeeRepository.existsById(employee.getId());
			System.out.println(isEmployeePresent);
			if (!isEmployeePresent) {
				throw new InvalidEmployeeException("Employee Details not found in database", employee.getId());
			} else {
				emp = employeeRepository.save(employee);
			}
		}
		return emp;
	}
}

In the updateEmployee method, we are throwing custom exception InvalidEmployeeException when the employee details are not present in the database, we will see how to exception handling in graphQL later.

Testing the mutation

Run the application and go to graphIQL playground at localhost:8080/graphiql.

Let try to create employee details using addEmployee mutation. use the below query variable that needed to be passed in the client query.


{
  "employee":{
      "name": "Priya",
      "jobTitle": "Support",
      "joinedDate": "20-05-2020",
      "gender": "female",
	    "mobileNo": "9840353535",
      "salary": 20000,
      "department": {
        "id": "1"
      },
      "project": {
        "id": "2"
      }
  }
}


graphQL mutation and custom error handling



we have successfully created an employee. now let try to update employee details.

graphQL mutation and internal server error


Exception Handling in GraphQL

We are going to see exception handling in graphQL.

GraphQL will always give a response (200 OK HTTP response) even if any error occurred during the processing of the request in that case graphQL will respond back with error details, the response contains two parts the data and errors.        

  •     Data - contains the result of the operation.
  •     errors - contain the errors which occurred during the operation.

The errors part contains below fields

  • message - description about the exception(error message)
  • path - The path where the error has occurred.
  • locations - contains the graph coordinates where an error has occurred, 
  • extensions - contains additional information about errors.

what will happen if exceptions are not handled in graphQL?

All the unhandled exceptions will be treated and sent as generic internal server errors to the client.


{
  "data": {
    "updateEmployee": null
  },
  "errors": [
    {
      "message": "Internal Server Error(s) while executing query",
    }
  ]
}


The above error message does not provide much information about what happened in the sever so we need to handle this exception properly and provide relevant information about the error to the client.

Custom Exception in GraphQL

In the updateEmployee method defined above if the employee is not found in the database then we are throwing an InvalidEmployeeException.
 
The InvalidEmployeeException class implement GraphQLError which is provided by graphql-java.

In the class, we have a map named as extensions which are used to store additional information to the client about the error.

For example, in our case, if an employee detail is missing in the database then we are throwing an exception where we can send the employee id which is missing in the database in the extension map so it helps the client to understand the reason for the error. 


public class InvalidEmployeeException extends RuntimeException implements GraphQLError {

	private static final long serialVersionUID = 1 L;

	private Map<String, Object> extensions = new HashMap<>();

	String message;

	public InvalidEmployeeException(String message, int id) {
		super(message);
		this.message = message;
		extensions.put("invalid employeeid", id);
	}

	@Override
	public String getMessage() {
		return message;
	}

	@Override
	public List<SourceLocation> getLocations() {
		return null;
	}

	@Override
	public List<Object> getPath() {
		return null;
	}

	@Override
	public Map<String, Object> getExtensions() {
		return extensions;
	}

	@Override
	public ErrorType getErrorType() {
		return ErrorType.DataFetchingException;
	}
}

 
Now if we send an employee id not present in the database for updating to the server, we will get a response like below.

graphQL custom error message using adapter


Adapter class for Error Handling

We are getting a complete stack trace in the error response if you don't want this behavior, you can use the adapter class to hide the stack trace.

public class GraphQLErrorAdapter implements GraphQLError {

	private GraphQLError error;

	public GraphQLErrorAdapter(GraphQLError error) {
		this.error = error;
	}

	@Override
	public Map<String, Object> getExtensions() {
		return error.getExtensions();
	}

	@Override
	public List<SourceLocation> getLocations() {
		return error.getLocations();
	}

	@Override
	public ErrorType getErrorType() {
		return error.getErrorType();
	}

	@Override
	public List<Object> getPath() {
		return error.getPath();
	}

	@Override
	public Map<String, Object> toSpecification() {
		return error.toSpecification();
	}

	@Override
	public String getMessage() {
		return (error instanceof ExceptionWhileDataFetching) ?
			((ExceptionWhileDataFetching) error).getException().getMessage() :
			error.getMessage();
	}
}

Now we will get responses like below.

graphQL custom error message using adapter

Create Error Handler

Create an error handler class and implement GraphQLErrorHandler, we need to override the processErrors method and write our logic.

@Component
public class ErrorHandler implements GraphQLErrorHandler {

	@Override
	public List<GraphQLError> processErrors(List<GraphQLError> errors) {
		List<GraphQLError> clientErrors = errors.stream()
			.filter(this::isClientError)
			.collect(Collectors.toList());

		List<GraphQLError> serverErrors = errors.stream()
			.filter(e -> !isClientError(e))
			.map(GraphQLErrorAdapter::new)
			.collect(Collectors.toList());

		System.out.println(clientErrors);
		System.out.println(serverErrors);
		List<GraphQLError> e = new ArrayList<>();
		e.addAll(clientErrors);
		e.addAll(serverErrors);
		return e;
	}

	protected boolean isClientError(GraphQLError error) {
		return !(error instanceof ExceptionWhileDataFetching || error instanceof Throwable);
	}
}

Here we are first collecting the client errors to a list and then converting the server errors to our adapter class finally adding all lists together and returning it.

You can get the source code on my Github graphql api.

Post a Comment

Previous Post Next Post

Recent Posts

Facebook