• Maikel Martens
    Maikel Martens

Sharing form validation rules between frontend and backend with Django

Almost every modern website has it: validation on a form as soon as you edit or leave the input. This gives the user immediate feedback, improving the user experience. However, for a developer, it can result in doing the work twice. Here's how we solved that.

As a developer, you don’t want to build the same validation in two places. This would mean that the frontend validation and backend validation must be kept in sync manually, meaning more work to maintain the application.

One of our customers wants to use frontend validation, and they’re right: They have large forms with very complex rules, and it would be unreasonable to show errors only after the form is submitted.

Also, because the rules are so complex, and there are many forms, it was not feasible to maintain all this validation separately for the frontend and the backend.

Because of this we preferred to define validation rules once. We looked at the following solutions:

  • Generate frontend validations based on backend validations.
  • Running frontend validations in the backend.
  • Using the backend validations in the frontend.

The first two solutions soon turned out to be too complex to use. So we started looking at re-using the backend validations in the frontend. This had the added advantage that we could use the standard Django forms that we are already familiar with.

In the end we only needed a small view with some javascript to get this working. Below we explain how it works with a simplified proof of concept.

Simplified proof of concept

The frontend validation works in the following way:

  1. When the page is loaded we search all forms and form fields, and listen for a change.
  2. As soon as a form field has been changed, we send the entire form plus the field for which we want to do validation.
  3. We catch the request for a field validation and return the validation message.
  4. Based on the response, the field is shown either in green, or in red with an error message.
Frontend validation gif

A working proof of concept can be found here. Below are the minimal code snippets to get the proof of concept working:

FormView

class ContactFormView(FormView):
    ...

    def post(self, request, *args, **kwargs):
        if "__field_name__" in request.POST:
            return self.validate_field(request)
        return super().post(request, *args, **kwargs)

    def validate_field(self, request):
        field_name = request.POST.get("__field_name__")
        form = self.form_class(request.POST)
        form.is_valid()
        return JsonResponse({
            "errors": form.errors.get(field_name, []),
        })

HTML

For the proof of concept we use django-crispy-forms. To get our errors in the right place in the page we need to add a wrapper element.

{% if help_text_inline and not error_text_inline %}
    {% include 'bootstrap5/layout/help_text.html' %}
{% endif %}

<div id="error-wrapper-{{ field.name }}">
    {% if error_text_inline %}
        {% include 'bootstrap5/layout/field_errors.html' %}
    {% else %}
        {% include 'bootstrap5/layout/field_errors_block.html' %}
    {% endif %}
</div>

Javascript

The little bit of javascript needed to enable validation.

import axios from "axios";

function validateField(formElement, fieldElement) {
  let formData = new FormData(formElement);
  formData.append('__field_name__', fieldElement.name);

  axios.post(formElement.action, formData).then(function (response) {
    let errors = response.data.errors;
    let errorsWrapperElement = document.getElementById(`error-wrapper-${fieldElement.name}`);
    if (errors.length === 0) {
      if (errorsWrapperElement) {
        errorsWrapperElement.innerHTML = "";
      }
      fieldElement.classList.remove('is-invalid');
      fieldElement.classList.add('is-valid');
    } else {
      if (errorsWrapperElement) {
        let errorsHtml = '';
        for (let i = 0; i < errors.length; i++) {
          errorsHtml += `<span class="invalid-feedback">${errors[i]}</span>`;
        }
        errorsWrapperElement.innerHTML = errorsHtml;
      }
      fieldElement.classList.remove('is-valid');
      fieldElement.classList.add('is-invalid');
    }
  });
}

export default function initFormValidation() {
  document.querySelectorAll("form").forEach(function (formElement) {
    formElement.querySelectorAll("[name]").forEach(fieldElement => {
      fieldElement.addEventListener("change", event => {
        validateField(formElement, fieldElement);
      });
    });
  });
}

Conclusion

It’s possible to re-use Django’s form validation rules in frontend code. This saves a lot of work and potential errors, compared to defining the form validation in two separate places.

We love code