Modern Django: Part 3: Creating an API and integrating with React

In the last post we developed a frontend for our note taking application which has the ability to store notes client-side using redux store. In this part we will create database models and APIs to create, read, update and delete notes in a database using react frontend and redux store.

The code for this repository is hosted on my github, v1k45/ponynote. You can checkout part-3 branch to see all the changes done till the end of this part.

Creating DB Models

To store the notes in database, first we need to create models. We'll start by creating an app and then a Note model inside it.

In the project root, create an app using the startapp command.

(ponynote)  $ ./manage.py startapp notes

Add notes.apps.NotesConfig to the INSTALLED_APPS list in ponynote/settings.py.

Note Model

Open notes/models.py add the following model:

from django.db import models


class Note(models.Model):
    text = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.text

Since our application feature is limited, two fields in the model will do.

Migrate the database to add this table using the following commands:

(ponynote)  $ ./manage.py makemigrations
Migrations for 'notes':
  notes/migrations/0001_initial.py
    - Create model Note

(ponynote)  $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, notes, sessions
Running migrations:
  Applying notes.0001_initial... OK

Building API using DRF

Now that the model is created, we can create an API to peform CRUD actions on the database. We'll do this using django-rest-framework.

Install django-rest-framework in the project virtual environment:

(ponynote)  $ pip install djangorestframework

Now create three python files, api.py, serializers.py and endpoints.py.

$ touch notes/api.py notes/serializers.py notes/endpoints.py

Start by creating a serializer for our Notes model, create the following serializer inside serializers.py:

from rest_framework import serializers

from .models import Note


class NoteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Note
        fields = ('id', 'text', )

The above serializer is ModelSerializer, it has an API similar (somewhat) to the ModelForm class in django.

After creating serializer, create an API for the Note model using NoteSerialzer:

from rest_framework import viewsets, permissions

from .models import Note
from .serializers import NoteSerializer


class NoteViewSet(viewsets.ModelViewSet):
    queryset = Note.objects.all()
    permission_classes = [permissions.AllowAny, ]
    serializer_class = NoteSerializer

A viewset works like a generic model view in django views. Lets allow all types of requests to this endpoint for now.

Lets register this API ViewSet to the DRF router and add it to django's urls. In the notes/endpoints.py write the following:

from django.conf.urls import include, url
from rest_framework import routers

from .api import NoteViewSet

router = routers.DefaultRouter()
router.register('notes', NoteViewSet)

urlpatterns = [
    url("^", include(router.urls)),
]

In project urls.py ponynote/urls.py:

from django.conf.urls import url, include
from django.views.generic import TemplateView

from notes import endpoints

urlpatterns = [
    url(r'^api/', include(endpoints)),
    url(r'^', TemplateView.as_view(template_name="index.html")),
]

After this, you'll be able to make requests on the notes API using curl or your favorite http client like Postman or Insomnia:

API in action

  • Create Note:
curl --request POST \
  --url http://localhost:8000/api/notes/ \
  --header 'content-type: application/json' \
  --data '{
    "text": "First Note!"
}'
  • Get All Notes:
curl --request GET \
  --url http://localhost:8000/api/notes/ \
  --header 'content-type: application/json'
  • Get a specific note using id:
curl --request GET \
  --url http://localhost:8000/api/notes/1/ \
  --header 'content-type: application/json'
  • Delete a specific note using id:
curl --request DELETE \
  --url http://localhost:8000/api/notes/1/ \
  --header 'content-type: application/json'

Integrating DRF API with Frontend

To be able to fetch and manipulate notes in the backend from the frontend, we need to make use of a few libraries. In order to fetch notes, we will use whatwg-fetch (already included with create-react-app) and redux-thunk for asynchronous action creation.

What is redux-thunk?

Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a condition is met. The inner function receives the store methods dispatch and getState as parameters.

Install and setup redux-thunk

$ npm install redux-thunk --save

Then in the App.js file, import thunk and applyMiddleware function, pass it to the createStore function and we are good to go.

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";

let store = createStore(ponyApp, applyMiddleware(thunk));

Async redux actions

Let us create our first async action using redux thunk. Write the following function in actions/notes.js:

export const fetchNotes = () => {
  return dispatch => {
    let headers = {"Content-Type": "application/json"};
    return fetch("/api/notes/", {headers, })
      .then(res => res.json())
      .then(notes => {
        return dispatch({
          type: 'FETCH_NOTES',
          notes
        })
      })
  }
}

The above code will perform an API call to the django application at api/notes/ and dispatch the FETCH_NOTES action when the response is received.

Now handle this action in the reducer reducers/notes.js by adding a FETCH_NOTES case in the switch statement.:

case 'FETCH_NOTES':
    return [...state, ...action.notes];

Since we are going to use the server database for notes, lets set the initialState as an empty array by removing the dummy note.

const initialState = [];

Using the async action in component

Using this action is no different than using a normal action. Just add it to mapDispatchToProps.

const mapDispatchToProps = dispatch => {
  return {
    fetchNotes: () => {
      dispatch(notes.fetchNotes());
    },
  }
}

And call that action dispatcher when component mounts so that the notes are fetched from the API and loaded into the redux store. Add componentDidMount method to PonyNote class:

componentDidMount() {
    this.props.fetchNotes();
}

On reload you should see the list of notes which you created using the API directly. If you haven't already, lets connect the addNote action to the API so that we start seeing notes directly from the database.

Add notes using API call

Lets update the addNote action in actions/notes.js file to send a POST request to the notes api:

export const addNote = text => {
  return dispatch => {
    let headers = {"Content-Type": "application/json"};
    let body = JSON.stringify({text, });
    return fetch("/api/notes/", {headers, method: "POST", body})
      .then(res => res.json())
      .then(note => {
        return dispatch({
          type: 'ADD_NOTE',
          note
        })
      })
  }
}

In the above action function, our application will send a POST request with JSON data of the note text and then disptach the ADD_NOTE action which will insert the added note to redux store. In reducers/notes.js, update the ADD_NOTE case to add the whole note object instead of text.

case 'ADD_NOTE':
    return [...state, action.note];

After this change our PonyNote.jsx component so that we reset the form only after the note has been successfully created. Add a return statement to the action dispatch call so that we can chain additional callbacks to the API call promise.

addNote: (text) => {
    return dispatch(notes.addNote(text));
},

Update the submitNote method by moving this.resetForm() call from the bottom to a callback for addNote function:

this.props.addNote(this.state.text).then(this.resetForm)

Similarly you can catch any errors thrown by the promise and handle API error and show them on the UI. To keep this post simple we will not cover that.

Updating notes

Updating notes is very similar to the addNote action which calls the API. All we need to do is pass the note.id in the url. Update the updateNote action in actions/notes.js:

export const updateNote = (index, text) => {
  return (dispatch, getState) => {

    let headers = {"Content-Type": "application/json"};
    let body = JSON.stringify({text, });
    let noteId = getState().notes[index].id;

    return fetch(`/api/notes/${noteId}/`, {headers, method: "PUT", body})
      .then(res => res.json())
      .then(note => {
        return dispatch({
          type: 'UPDATE_NOTE',
          note,
          index
        })
      })
  }
}

Note that the first argument of updateNote is index instead of id, this is done to easily get the note which is being updated. Also, we have a getState argument for the return action function, it is used to get the current state of the application. We used it to get the note.id using the index we had.

Another important thing is that the request method is PUT, which indicates that the resource on the server should be updated. In the final action dispatch we have the index and newly saved note as the data, we will use it in the reducer.

Update the UPDATE_NOTE case in redcers/notes.js:

case 'UPDATE_NOTE':
    let noteToUpdate = noteList[action.index]
    noteToUpdate.text = action.note.text;
    noteList.splice(action.index, 1, noteToUpdate);
    return noteList;

In PonyNote.jsx, update mapDispatchToProps and submitNote method like we did for adding notes.

updateNote: (id, text) => {
    return dispatch(notes.updateNote(id, text));
},
this.props.updateNote(this.state.updateNoteId, this.state.text).then(this.resetForm);

Now you'll be able to update notes and save it in the database.

Deleting Notes

Update actions/notes.js's deleteNote action to send a DELETE request to the API server:

export const deleteNote = index => {
  return (dispatch, getState) => {

    let headers = {"Content-Type": "application/json"};
    let noteId = getState().notes[index].id;

    return fetch(`/api/notes/${noteId}/`, {headers, method: "DELETE"})
      .then(res => {
        if (res.ok) {
          return dispatch({
            type: 'DELETE_NOTE',
            index
          })
        }
      })
  }
}

In the DELETE_NOTE reducer in reducers/notes.js make the following changes:

case 'DELETE_NOTE':
    noteList.splice(action.index, 1);
    return noteList;

Summary

Now you'll be able to create, read, update and delete notes using the API built using django-rest-framework. All the notes are stored in redux store client-side, changes will be reflected in the database and persist on reload.

In the next part we'll add authentication to pony note so that multiple users can maintain their notes privately. We'll implement login/signup flow and associate notes with users.

Reference