Build a CRUD Django REST API
Tags: django, python, tutorial
Published: 2020-03-09 21:30:36

In this article, we'll be adding CRUD(Create, Read, Update, Delete) functionality to an already existing Django REST API with user authentication. This is a continuation of a previous article where we added authentication functionalities like register, login, logout to a simple Bookstore Django REST API with just one endpoint that sends a response `{"message": "Welcome to the BookStore!"}` and a user must be authenticated to access our endpoint. 

Do you want to:
- [Create a simple REST API with Django?](https://dev.to/nobleobioma/create-a-simple-rest-api-with-django-253p)
- [Add Authentication to a REST Framework Django API?](https://dev.to/nobleobioma/adding-authentication-to-a-rest-framework-django-api-1e5a)

## Let's get started 😀
We'll start by creating some models. Django models are basically python objects that are utilized in accessing and managing data. Models define the structure of stored data like field types, creating relationships between data, applying certain constraints to data fields and a lot more. For more information on the Django Model, check the [documentation](https://docs.djangoproject.com/en/3.0/ref/models/) v3.

For our `bookstore_app`, we'll create two models `Author` and `Book`.

```python
# ./bookstore_app/api/models.py

from django.db import models
from django.conf import settings
from django.utils import timezone

# Create your models here.
class Author(models.Model):
  name = models.CharField(max_length=200)
  added_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  created_date = models.DateTimeField(default=timezone.now)

  def __str__(self):
    return self.name

class Book(models.Model):
  title = models.CharField(max_length=200)
  description = models.CharField(max_length=300)  
  author = models.ForeignKey(Author, on_delete=models.CASCADE)
  added_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  created_date = models.DateTimeField(default=timezone.now)

  def __str__(self):
    return self.title

```
Somethings to take note of on the models we just created:
1. In the `Book` model, a book must have an author. So we created a field `author` which is a ForeignKey referencing the `Author` model.
2. We want to keep track of the user that added the entry for either `Book` or `Author`, so we create a field `added_by` which is a ForeignKey referencing the `AUTH_USER_MODEL`.


Now we have our models created, we'll have to run migrations. But before that let's `makemigrations` after which we'll then run the created migrations.

```bash
$ python manage.py makemigrations
$ python manage.py migrate
```
Time for a test-drive 🚀. I'll be testing my newly created models in the Django `shell` by adding some entries to `Author`. On the terminal, let's start the shell by running `python manage.py shell`.

```bash
$ python manage.py shell
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
```
Looking at the fields in the `Author` models, we have:
- `name`: which is a character field with a max length of 200, so it can take strings.
- `added_by`: which is referencing the User model. So to make a new entry, we need to pass an instance of a user. 
- `created_date`: which defaults to the current entry time.

So, in the `shell` we have to import the `User` and `Author` models to make adding an entry to `Author` possible.

```bash
>>> from django.contrib.auth.models import User
>>> from api.models import Author
``` 
##### Let's create a user:
First, we make an instance of User, then we call the save method on the model to save to the Db.
```bash
>>> user = User(first_name="John", last_name="Doe", username="johndoe")
>>> user.save()
```

##### Adding an Author:
To add an author, we make an instance of Author, passing the instance of the user we already created to `added_by`
```bash
>>> author1 = Author(name="Author Mie", added_by=user)
>>> author1.save()
>>> author2 = Author(name="Author Mello", added_by=user)
>>> author2.save()
```

We have successfully added two new authors. To get all entries on the Authors table:

```bash
>>> Author.objects.all()
<QuerySet [<Author: Author Mie>, <Author: Author Mello>]>
```
We can also with our models through the Django admin interface provided by Django which is accessible at https://localhost:8000/admin. But before we do that we'll first have to:

1. Add our models to admin interface
2. Create a superuser

#### To add the models to the Admin Interface

```python
# bookstore_app/api/admin.py

from django.contrib import admin
from .models import Author, Book

# Register your models here.
admin.site.register(Author)
admin.site.register(Book)
```

#### To create a superuser
> A "superuser" account has full access to the server and all needed permissions.

On the terminal, run `python manage.py createsuperuser`

```bash
$ python manage.py createsuperuser
Username: superadmin
Email address: superadmin@email.com
Password: 
Password (again):

Superuser created successfully.
```

We have successfully created a superuser. Now, run the server and login to the admin page on the browser using the superuser credentials that you created. After a successful login, your admin interface will look like the image below. You can now add more Authors and Books even set permissions, disable certain users and lots more if need be. Of course, you are the `superuser`!!!

https://localhost:8000/admin
![Django Admin Interface](https://dev-to-uploads.s3.amazonaws.com/i/hnu4gn103pm3ytdfk6jq.png)


So far, we have been able to persist our data and read from the DB on the shell. It's time to create some views to handle POST, GET, PUT, DELETE requests on the server. But before we start adding new views in the `api/views.py` file, let's create serializers for our models. 

> Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON.

To begin creating our serializers, let's create a `serializers.py` file in our `api` app folder and then create our `AuthorSerializer` and `BookSerializer`, selecting the fields that we care about in the different models that we will pass to the response.

```python
# bookstore_app/api/serializers.py

from rest_framework import serializers
from .models import Author, Book

class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ['id', 'name', 'added_by', 'created_by']

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'description', 'created_date', 'author', 'added_by']

``` 

We have our serializer ready, let's open the `api/views.py` file. The current content of the file should be from the previous post, [Adding Authentication to a REST Framework Django API](https://dev.to/nobleobioma/adding-authentication-to-a-rest-framework-django-api-1e5a).

```python
# ./bookstore_app/api/views.py

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt

@api_view(["GET"])
@csrf_exempt
@permission_classes([IsAuthenticated])  
def welcome(request):
    content = {"message": "Welcome to the BookStore!"}
    return JsonResponse(content)
```


#### User can get all books

```python
# ./bookstore_app/api/views.py

...
from .serializers import BookSerializer
from .models import Book
from rest_framework import status


@api_view(["GET"])
@csrf_exempt
@permission_classes([IsAuthenticated])
def get_books(request):
    user = request.user.id
    books = Book.objects.filter(added_by=user)
    serializer = BookSerializer(books, many=True)
    return JsonResponse({'books': serializer.data}, safe=False, status=status.HTTP_200_OK)
```
#### User can add a book

```python
# ./bookstore_app/api/views.py

...
from .models import Book, Author
import json
from django.core.exceptions import ObjectDoesNotExist

@api_view(["POST"])
@csrf_exempt
@permission_classes([IsAuthenticated])
def add_book(request):
    payload = json.loads(request.body)
    user = request.user
    try:
        author = Author.objects.get(id=payload["author"])
        book = Book.objects.create(
            title=payload["title"],
            description=payload["description"],
            added_by=user,
            author=author
        )
        serializer = BookSerializer(book)
        return JsonResponse({'books': serializer.data}, safe=False, status=status.HTTP_201_CREATED)
    except ObjectDoesNotExist as e:
        return JsonResponse({'error': str(e)}, safe=False, status=status.HTTP_404_NOT_FOUND)
    except Exception:
        return JsonResponse({'error': 'Something terrible went wrong'}, safe=False, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
```
#### User can update a book entry by id

```python
# ./bookstore_app/api/views.py

...
@api_view(["PUT"])
@csrf_exempt
@permission_classes([IsAuthenticated])
def update_book(request, book_id):
    user = request.user.id
    payload = json.loads(request.body)
    try:
        book_item = Book.objects.filter(added_by=user, id=book_id)
        # returns 1 or 0
        book_item.update(**payload)
        book = Book.objects.get(id=book_id)
        serializer = BookSerializer(book)
        return JsonResponse({'book': serializer.data}, safe=False, status=status.HTTP_200_OK)
    except ObjectDoesNotExist as e:
        return JsonResponse({'error': str(e)}, safe=False, status=status.HTTP_404_NOT_FOUND)
    except Exception:
        return JsonResponse({'error': 'Something terrible went wrong'}, safe=False, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
```
#### User can delete a book entry by id

```python
# ./bookstore_app/api/views.py

...

@api_view(["DELETE"])
@csrf_exempt
@permission_classes([IsAuthenticated])
def delete_book(request, book_id):
    user = request.user.id
    try:
        book = Book.objects.get(added_by=user, id=book_id)
        book.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
    except ObjectDoesNotExist as e:
        return JsonResponse({'error': str(e)}, safe=False, status=status.HTTP_404_NOT_FOUND)
    except Exception:
        return JsonResponse({'error': 'Something went wrong'}, safe=False, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
```
Having completed the views and its functionalites, we'll now add them to the `api/urls.py` file.

```python
# ./bookstore_app/api/urls.py

from django.urls import include, path
from . import views

urlpatterns = [
  ...
  path('getbooks', views.get_books),
  path('addbook', views.add_book),
  path('updatebook/<int:book_id>', views.update_book),
  path('deletebook/<int:book_id>', views.delete_book)
]
```

Now, let's get our environment and Django server started. To access the `manage.py` file, you have to be in the django project `bookstore_app` directory.

```bash
$ cd bookstore_app
$ pipenv shell
$ python manage.py runserver
``` 
You can use [Postman](https://www.postman.com/) to test with the same JSON properties, but I'll be using [curl](https://curl.haxx.se/).

### Let the tests begin 😀

*Register a new user*

To create a user, we will be making a `POST` request to `localhost:8000/registration/` and passing fields `username`, `password1`, `password2`, you may choose to pass an `email` field but that is optional.

```bash
> Request

$ curl -X POST -H "Content-Type: application/json" -d '{"username":"testuser", "password1":"testpassword", "password2":"testpassword"}' localhost:8000/registration/

> Response:
   {"key":"1565c60a136420bc733b10c4a165e07698014acb"}
```
You also get an authentication token after a successful login `localhost:8000/login/` passing fields `username` and `password`. To test the rest of the endpoints, we need to prove to the server that we are valid authenticated users. So to do this we'll set the token we got after registration to the `Authorization` property in the `Headers` dict prefixing the actual token with `Token`.

```
Authorization: Token 1565c60a136420bc733b10c4a165e07698014acb
```

*Add a new book*
To add a book, we make a `POST` request to `localhost:8000/api/addbook` passing fields `title`, `description`, `author`(id of an author we had earlier created)

```bash
> Request
$ curl -X POST -H "Authorization: Token 1565c60a136420bc733b10c4a165e07698014acb" -d '{"title":"CRUD Django", "description":"Walkthrough for CRUD in DJANGO", "author": 1}' localhost:8000/api/addbook 

> Response
   {"book": {
       "id": 1, 
       "title": "CRUD Django", 
       "description": "Walkthrough for CRUD in DJANGO", 
       "author": 1, 
       "added_by": 2, 
       "created_date": "2020-02-29T21:07:27.968463Z"
     }
   }
```
*Get all books*
To get all books, we'll make a `GET` request to localhost:8000/api/getbooks. This will give us a list of all book that has been added by the currently logged in user.

```bash
> Request
$ curl -X GET -H "Authorization: Token 9992e37dcee4368da3f720b510d1bc9ed0f64fca" -d '' localhost:8000/api/getbooks

> Response
{"books": [
      {
        "id": 1, 
        "title": "CRUD Django", 
        "description": "Walkthrough for CRUD in DJANGO", 
        "author": 1, 
        "added_by": 2, 
        "created_date": "2020-02-29T21:07:27.968463Z"
       }
    ]
 }  
```

*Update a book-entry by id*
To update a book, we make a `PUT` request passing the `id` of the book we want to update as a parameter on the URL to `localhost:8000/api/updatebook/<id>` passing fields the fields you want to alter.

```bash
> Request
$ curl -X PUT -H "Authorization: Token 9992e37dcee4368da3f720b510d1bc9ed0f64fca" -d '{"title":"CRUD Django Updated V2", "description":"Walkthrough for CRUD in DJANGO", "author": 1}' localhost:8000/api/updatebook/1

> Response
{"book": {
    "id": 1, 
    "title": "CRUD Django Updated V2", 
    "description": "Walkthrough for CRUD in DJANGO", 
    "author": 1, 
    "added_by": 2, 
    "created_date": "2020-02-29T21:07:27.968463Z"
  }
}  
```

*Delete a book-entry by id*
To delete a book, we make a `DELETE` request passing the `id` of the book we want to delete as a parameter on the URL to `localhost:8000/api/deletebook/<id>`

```bash
> Request
$ curl -X DELETE -H "Authorization: Token 9992e37dcee4368da3f720b510d1bc9ed0f64fca" -d '' localhost:8000/api/deletebook/1  
```

Hurray🎉🎉, we have a fully functional CRUD Django REST API. If you are testing using postman you might run into an error response `{ "detail": "CSRF Failed: CSRF token missing or incorrect." }`, clearing `Cookies` on Postman will fix the issue.

All our code for the last two(2) posts and this post:
- [Create a simple REST API with Django?](https://dev.to/nobleobioma/create-a-simple-rest-api-with-django-253p)
- [Add Authentication to a REST Framework Django API?](https://dev.to/nobleobioma/adding-authentication-to-a-rest-framework-django-api-1e5a)
- [Build a CRUD Django REST API](https://dev.to/nobleobioma/build-a-crud-django-rest-api-46kc)

resides in this Github repository, [Bookstore Django REST API](https://github.com/nobioma1/ah_bk_django_rest_api)

Thank you for reading. 😃