Fetching Data with AJAX and Django

When using Django to serve webpages, it passes complete HTML templates to the browser anytime a user performs an action that causes the page to change, even if that change only affects a small portion of the page. But it isn't necessary to re-render the page completely if we only want to update part of the page - we can use AJAX instead.

AJAX provides a way to send a GET or POST request to a Django view and receive any returned data without a page refresh. Modern JavaScript includes the fetch API which gives us a pure JavaScript way to send AJAX requests.

Let's look at how we can make GET and POST requests with fetch to pass JSON data between the view and the template.

GET Request

Making a GET request with fetch

A GET request with fetch is made by supplying it the URL of the view and the appropriate headers. Once the request is made and the view returns the requested data, the response then needs to be converted to JSON before it is made available for further actions.

fetch(URL, {
    headers:{
        'Accept': 'application/json',
        'X-Requested-With': 'XMLHttpRequest', //Necessary to work with request.is_ajax()
    },
})
.then(response => {
    return response.json() //Convert response to JSON
})
.then(data => {
    //Perform actions with the response data from the view
})

URL

fetch takes a URL as its first argument. Depending on how the Django project's URLconfs and views are configured, the URL may have keyword arguments or query strings that we want to use in the view to select the requested data.

Headers

An AJAX request made with fetch will include several headers. We expect the data returned from the view in JSON form, so we set the 'Accept' header to 'application/json'. In the view, we may want to make sure the request is an AJAX request. By including the 'X-Requested-With' header set to 'XMLHttpRequest', the view will be able to check if the request is AJAX or not.

fetch doesn't return data directly. Instead, it returns a promise that will be fulfilled and resolve to the requested response. To get the data from the response, we have to use chained promises by using the .then handler multiple times. The first .then takes the resolved response and converts it to JSON. The second .then gives us access to the data returned by the first .then and allows us to use it however we want to update the page.

Handling GET requests in the view

We need a view to handle the AJAX request from the fetch call. This can be done in multiple ways, but one of the simplest is to use a function-based view that takes the request and returns a JsonResponse with the requested data.

# views.py
from django.http import JsonResponse

def ajax_get_view(request): # May include more arguments depending on URL parameters
    # Get data from the database - Ex. Model.object.get(...)
    data = {
            'my_data':data_to_display
    }
    return JsonResponse(data)

those would also be included in the function parameter list along with request. Data would be retrieved from the database depending on those URL parameters or query strings if any were used. The data we want to send back to the page must be in a dictionary that is provided to JsonResponse. Make sure to import JsonResponse from django.http before calling it.

The view will return the JsonResponse, which serializes the data dictionary and sends it back to our page where the processing through chained promises takes place. We could now use JavaScript to update a portion of the page with the data from the GET request.

POST Request

Making a POST request with fetch

POST requests with fetch require more parameters than GET requests.

fetch(URL, {
    method: 'POST',
    credentials: 'same-origin',
    headers:{
        'Accept': 'application/json',
        'X-Requested-With': 'XMLHttpRequest', //Necessary to work with request.is_ajax()
        'X-CSRFToken': csrftoken,
},
    body: JSON.stringify({'post_data':'Data to post'}) //JavaScript object of data to POST
})
.then(response => {
        return response.json() //Convert response to JSON
})
.then(data => {
//Perform actions with the response data from the view
})

Method

fetch defaults to making a GET request. We have to explicitly tell it to make a POST request by adding method: 'POST'.

Credentials

We need to specify how to send credentials in the request. Credentials can be tricky especially if the frontend and backend of a project are hosted separately. If the AJAX request is being made from a template that is served from the same location as the rest of the backend, we can use the default value of 'same-origin'. This means that user credentials will be sent in the request if the URL being requested is from the same site as the fetch call. If the frontend and backend are not at the some location, a different credential setting would be needed and Cross-Origin Resource Sharing (CORS) needs to be taken into account.

Headers

The 'Accept' and 'X-Requested-With' headers are the same as those of the GET request, but now an additional 'X-CSRFToken' header must be included.

When making a POST request to Django, we need to include the the csrf token to prevent Cross Site Request Forgery attacks. The Django docs give the exact JavaScript code we need to add to get the token from the csrftoken cookie.

function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
const csrftoken = getCookie('csrftoken');

Now that we have the csrftoken, we add it to the headers as 'X-CSRFToken': csrftoken.

Body

The goal of a POST request is to send data to a view and update the database. This means we also need to include the data in the fetch call. Assuming we want to send JSON data, we add body: JSON.stringify(data) where data is a JavaScript object of the data we want to send. Other data can be sent in the body besides JSON data including files and data from forms. See the MDN docs for more information about how to include other types of data.

The response we get back from the POST request is handled with chained promises exactly like a GET request.

Handling POST requests in the view

The view that accepts the POST request will get the data from the request, perform some operation with it, and return a response.

# views.py
from django.http import JsonResponse
import json

    def ajax_post_view(request):
        data_from_post = json.load(request)['post_data'] #Get data from POST request
        #Do something with the data from the POST request
        #If sending data back to the view, create the data dictionary
        data = {
            'my_data':data_to_display,
        }
        return JsonResponse(data)

In our view, we need to extract the data from the AJAX request to be able to use it. The data was sent as JSON, so we need to load it into our view by using json.load(request). This requires importing the json module from the Python standard library. The result is a dictionary of the data we sent with fetch. Now we can access the data through its key.

Once we have the data from the request, we can do what we need to perform the action the user wanted that initiated the AJAX request. This could be creating a new instance of a model or updating an existing one.

Like with a GET request, data can be sent back to the page using JsonResponse and a dictionary with the data. This could be the new or updated model object or a success message.

Making sure a request is AJAX

Most of the time an AJAX request is being made because we only want a portion of the page to be updated and need to get new data to make the update. The data returned by JsonResponse has little use on its own outside of the context of the page. If we don't setup the view correctly though, the data could be accessed outside of an AJAX request and won't be presented to the user as we expect it to.

To prevent this from happening, we can add a check in the view to make sure the request is an AJAX request by using the request.is_ajax() method.

# views.py
from django.http import JsonResponse

def ajax_view(request):
    if request.is_ajax():
        data = {
                'my_data':data_to_display
        }
        return JsonResponse(data)

This uses the 'X-Requested-With' header to determine if the request was initiated with AJAX or not. If we tried to access this view by typing the URL directly into the browser, we would get an error. Additional logic could be added to the view (such as a redirect) to prevent the user from seeing an error if they try to access the view without an AJAX request.

Django 3.1 and Beyond

In the upcoming Django 3.1 release (August 2020), request.is_ajax() will be deprecated. This means we have to recreate the functionality ourselves if we want to check for an AJAX request. Luckily, the Django developers tell us exactly what we need to do. We have to recreate the logic from the request.is_ajax() method ourselves, which is only 1 line of code:

request.headers.get('x-requested-with') == 'XMLHttpRequest'

Now we can edit the view to include this check:

def ajax_view(request):
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
    # Get requested data and create data dictionary
    return JsonResponse(data))

Some Important Notes

While fetch is a convenient way to make AJAX requests, it isn't supported in all browsers, namely all version of Internet Explorer. If you need to support IE, look into jQuery or XMLHttpRequest to make AJAX requests.

AJAX requests should be limited to small portions of a Django project. If you find yourself using them in multiple templates to get large amounts of data, look into creating an API with Django Rest Framework.

Summary

By using AJAX requests in our Django projects we can change parts of a page without needing to reload the entire page. The fetch API makes adding this functionality fairly painless while requiring minimal JavaScript. Used correctly and sparingly, it can make our pages feel faster and give the users a more interactive experience.

Helpful Resources

Questions, comments, still trying to figure it out? - Let me know!