Introduction

Application security is an absolute necessity and it must be a top priority. To make our application secure, we must do server-side data validation. Because we can’t rely on client-side input validation. Certainly, client-side input validation can be manipulated in many ways, which will make our system vulnerable. Therefore, the same input validation must be performed on the server-side.

Any application can have errors. So, handling errors and returning back some informative messages to the end-user is very important. GraphQL does not send any status code with a response like REST. It sends an array called errors with the details of the error. The location and the path of the error are also included. So, handling errors in GraphQL looks pretty simple. But you may need custom error handling with server-side data validation. We will cover both types of error handling and server-side data validation in the following.

Server-Side Data Validation In Django-Graphene

With Graphene, there is no standard way to do server-side input validation. However, we will share a simple and modular way to do server-side validation. In our previous article, we created a blog app without server-side data validation. Here, we will add some validations for this app.

We will add a utility class called FormValidator as an abstract base validator. To add this, create a file inside the blog app called utils.py. And add the following code to this file.

import inspect
from abc import ABCMeta, abstractmethod


class FormValidator:
    __metaclass__ = ABCMeta

    def __init__(self, data):
        self.data = data
        self.cleaned_data = {}
        self.errors = {}

    def is_valid(self):
        for name, validator in inspect.getmembers(self, predicate=inspect.ismethod):
            if "validate_" in name:
                validator()
        return not self.errors

    @abstractmethod
    def save(self):
        pass

Error Types

To return errors on the server-side, we can use GraphQL’s default GraphQLError handler. Besides the default one, we can also define custom fields for errors. So, add the following code to blog/schema/blogs.py for custom error fields.

class BlogErrorsInputType(graphene.ObjectType, BlogFields):
     id = graphene.String()
     author_id = graphene.String()

Custom Validation Forms

To use our base validation class, create a file called forms.py in blog app. And add the following code.

from .utils import FormValidator
from .models import Blog, Author


class CreateBlogForm(FormValidator):
    
    def validate_author_id(self):
        author_id = self.data.get("author_id", None)
        if not author_id:
            return self.errors.update({"author_id":"The author_id feild is required."})

        if not Author.objects.filter(id=author_id).exists():
            return self.errors.update({"author_id":"Author {author_id} does not exist".format(author_id=author_id)})
        
        self.cleaned_data["author_id"] = author_id         

    def save(self):
        data = self.data
        data.update(self.cleaned_data)

        blog = Blog()
        for key, val in data.items():
            setattr(blog, key, val)
        blog.save()
        return blog

Here we are validating author_id with validate_author_id method. This method will be called automatically by our base class when we will call is_valid method. It is almost similar to how Django form and its validator works.

Now go to the blog/schema/blogs.py file and import the CreateBlogForm like this.

from ..forms import CreateBlogForm

And update the CreateBlog mutation (line: 47) with the following code.

class CreateBlog(graphene.Mutation):
    class Arguments:
        input = BlogInputType(required=True)

    ok = graphene.Boolean()
    blog = graphene.Field(BlogType)
    errors = graphene.Field(BlogErrorsInputType)

    @staticmethod
    def mutate(root, info, input):
        form = CreateBlogForm(data=input)
        if form.is_valid():
            blog = form.save()
            return CreateBlog(ok=True, blog=blog, errors=form.errors)
        return CreateBlog(ok=False, errors=form.errors)

Here we used our custom CreateBlogForm and checked the input validation with form.is_valid() mehtod. If you are familiar with Django/DRF, it should look very simple to you.

Testing Server-side Data Validation

We are done. It’s time to test our custom validator. So, open your GraphQL console in your browser and add the following mutation.

mutation createBlog {
  createBlog(input: {
    title: "How to build GraphQL API with Django.",
    authorId: 100,
    body: "GraphQL is a declarative, strongly typed, data-driven query language to build APIs."
  }) {
    ok
    errors{
      id
      authorId
      title
      body
    }
    blog {
      id
      title
      author {
        id
        name
      }
      body
    }
  }
}

Here we added 100 as author_id which does not exist. Our validator should handle this and return an informative error message for author_id field. If you run the above mutation, you should see the following result.

{
  "data": {
    "createBlog": {
      "ok": false,
      "errors": {
        "id": null,
        "authorId": "Author 100 does not exist",
        "title": null,
        "body": null
      },
      "blog": null
    }
  }
}

Cool! Let’s add another validation method to our form to check the minimum length of our blog title. Update the CreateBlogForm with the following code.

def validate_title(self):
        title = self.data.get("title", None)
        if not title:
            return self.errors.update({"title":"The title feild is required."})
        if len(title) < 10:
            return self.errors.update({"title":"Title is too short."})
        
        self.cleaned_data["title"] = title 

Now make the blog title too short. For example: add Test blog as a title and run the mutation again. You should see the following result.

{
  "data": {
    "createBlog": {
      "ok": false,
      "errors": {
        "id": null,
        "authorId": "Author 100 does not exist",
        "title": "Title is too short.",
        "body": null
      },
      "blog": null
    }
  }
}

Cool! We are getting validation error per field and can show it to the end-user easily. You can add more validations for other fields and it will work automatically.

Conclusion

To ensure application security, we must have server-side input validation. We added a simple base class to do server-side validation with Graphene-Django and added custom form handling mechanism per field.

If you would like to see the full source code, check it out from here.