# Your first Django REST API

## Requirements

* Basic knowledge of Python
    
* Basic understanding of Django framework
    

## REST API

Rest API is a term concatenated from 2 abbreviations.

* REST stands for REpresentational State Transfer
    
* API stands for Application Programming Interface
    

REST is an architectural style for distributed hypermedia systems that abide by some rules.

* Client and server sides are separated.
    
* Each request from the client to the server must contain all of the information (it is stateless).
    
* Interface is uniformed (all the API points should be accessible by a similar approach).
    

API is a specification by some software that allows other programs to interact with it. It allows a developer to make a specific request to send or receive information.

**So, REST API is a web service for programs that uses REST architectural style and abides by its rules.**

## Django REST Framework

Django REST Framework is a powerful tool for creating REST APIs based on Django. It comes with built-in serialization, authentication and it's highly customizable. Besides that, APIs built with it are browsable out of the box. That means that you're able to use it almost like a simple web application. You can add data and view it in a browser.

Another great thing is that it comes with [great documentation](https://www.django-rest-framework.org/).

## Squats, pushups, and API

A bunch of programmer friends decided they want to do more for their bodies. They plan to do as many pushups and squats as they can each day and compete against each other. The person that does the most of both at the end of the period, gets a reward.

Since they're programmers, each of them will create their own client and analyze data in their own way. To be able to see where the competitors are, they need an API. They'll send their daily record to the API and will be able to retrieve data about others back.

Each day, they'll post how many pushups and squats they did, but they don't need daily records of others back. They just need to know the total count of both for every other competitor.

## Initial setup

Start with creating a project and virtual environment:

```sh
$ mkdir health_challenge
$ python3.9 -m venv env
$ source env/bin/activate
```

Next, install Django and Django Rest Framework:

```sh
(env)$ pip install django
(env)$ pip install djangorestframework
```

After that, create a new Django project and app:

```sh
(env)$ django-admin.py startproject tutorial .
(env)$ django-admin.py startapp challenge
```

> adding a dot after the name of the project prevents Django from creating a redundant parent directory

Once the project is created, register Django Rest Framework (DRF) and the challenge app to installed apps inside "tutorial/settings.py" :

```python
# tutorial/settings.py
INSTALLED_APPS = [
    # ...
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'challenge',
]
```

## Models

Create a Record model inside the "challenge/models.py":

```python
# challenge/models.py
from django.utils import timezone
from django.db import models

class Record(models.Model):
    date = models.DateField(default=timezone.now)
    pushups = models.PositiveIntegerField(default=0)
    squats = models.PositiveIntegerField(default=0)

    def __str__(self):
        return self.date
```

For now, it has 3 attributes: date, pushups, and squats. You'll add the creator later.

Create an initial migration and apply it:

```shell
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
```

Register the Record model in `challenge/admin.py` so you can access it from the Django admin panel:

```python
# challenge/admin.py
from django.contrib import admin
from challenge.models import Record

admin.site.register(Record)
```

If you want to access the admin panel to check your work, you have to create a superuser:

```sh
(env)$ python manage.py createsuperuser
```

Enter the required data (remember the password!).

Now, run the local server for the very first time:

```sh
(env)$ python manage.py runserver
```

Navigate to the admin panel in your application: http://127.0.0.1:8000/admin/ and open Records. Add 2 new records.

## Serializers

Instead of a page composed of HTML, CSS, and JS, an API only provides **raw data**. That data needs to be in some kind of form that it's easy for a program to extract it (`JSON`/`XML`). In DRF, Serializer takes care of that. DRF Serializer covers serialization and deserialization of data and its validation. There are two possible classes to use when you need a serializer. `Serializer` class is a powerful generic way to control the output, `ModelSerializer` is a specialized serializer for model instances. Considering you're working with your models, you'll use the second one.

Create a new file `serializers.py` inside *challenge* directory:

```python
# challenge/serializers.py
from rest_framework import serializers
from .models import Record


class RecordSerializer(serializers.ModelSerializer):
    class Meta:
        model = Record
        fields = ("id", "date", "pushups", "squats")
```

`ModelSerializer`class creates a `Serializer` with a set of fields that corresponds to the selected `Model`. As you can see, you had to import the serializer and the *Record* model.

Inside the `ModelSerializer` there's a `class Meta` in which you specify which model you want to use and which fields from the Model to serialize.

> Even if you want to serialize all the data, it is recommended to specify each field explicitly. That saves you from exposing too much data by accident if you change your `Model`. Ignoring that, you can specify all the fields with `fields = "__all__"`.

## View

Since you're not building a typical website, you won't use a classical Django `View`. DRF provides a `View` subclass - `APIView`.

`GenericAPIView` which extends `APIView`, provides concrete generic views that cover commonly required behaviors for lists and detail views.

There are quite a few concrete generic views you could use:

* **CreateAPIView** is used for resource creation endpoints
    
* **ListCreateAPIView** is used for a read-write list of resources
    
* **RetrieveUpdateDestroyAPIView** is used for read-write-delete on single resource
    
* ...
    

> You can check all of them [here](https://www.django-rest-framework.org/api-guide/generic-views/#concrete-view-classes)

Since you're creating an endpoint for posting the records, you'll use **CreateAPIView** generic view. Create a new file *view.py*:

```python
from rest_framework import generics
from .serializers import RecordSerializer


class RecordAdd(generics.CreateAPIView):
    serializer_class = RecordSerializer
```

There is no data to retrieve, so you just need to set the `serializer_class` to the previously created `RecordSerializer`.

## URL

The last thing to do is to add an url where you can access your view. Create an `urls.py` inside the *challenge* folder.

```python
# challenge/urls.py
from django.urls import path
from .views import RecordAdd

urlpatterns = [
    path('add-record', RecordAdd.as_view())
]
```

After that, include that file inside "tutorial/urls.py":

```python
#tutorial/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("challenge.urls")),
]
```

Now you can navigate to http://127.0.0.1:8000/add-record and voila:

![record_add_view.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368590383/oO7j_0ozN.png align="left")

Your first API endpoint!

Add a record and go check it in the admin panel: http://127.0.0.1:8000/admin/challenge/record/. You should see the record you entered via API!

## Record owners

If your app is about the competition between record creators, you need to be able to assign created records to a user. That's how you'll know who made that many pushups.

Start by changing a *Record* model. You need to add a creator field to it.

```python
# challenge/models.py
# ...

class Record(models.Model):
    date = models.DateField(default=timezone.now)
    pushups = models.PositiveIntegerField(default=0)
    squats = models.PositiveIntegerField(default=0)
    creator = models.ForeignKey('auth.User', on_delete=models.CASCADE)

# ...
```

Stop the local server with `Ctrl+C` and run `(env)$ python manage.py makemigrations`. Since you've already created some records it won't go as easy as the first time.

You'll get asked the following:

```sh
You are trying to add a non-nullable field 'creator' to record without a default; you can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
```

Select 1 and type 1 in again, so your superuser will be the creator of current records. Sync your database with `(env)$ python manage.py migrate`.

Make some minor changes, so you can check if your `Model` change works as it should.

First, create another superuser: `shell (env)$ python manage.py createsuperuser` Next, add a login to the browsable API. Include `rest_framework.urls` in the "tutorial/urls.py":

```python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
  path('admin/', admin.site.urls),
  path('api-auth/', include('rest_framework.urls')),
  path("", include("challenge.urls")),
]
```

If you refresh the page (http://127.0.0.1:8000/add-record), you'll see a little arrow next to your username on the right side of the page:

![login.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368621124/ti61WjKOA.png align="left")

Click on it and you can logout and login with the other superuser you created.

After that, change the list display for records admin. Add a *RecordAdmin* with the `list_display` specified to the "challenge/admin.py":

```python
# challenge/admin.py
from django.contrib import admin
from challenge.models import Record

class RecordAdmin(admin.ModelAdmin):
    list_display = ('date', 'creator')

admin.site.register(Record, RecordAdmin)
```

> This is not necessary, solely intent of this is that you can easily see who's the records creator.

Now, logged in as another user, try to create a new record via browsable API and... Ups, you've got an error.

```plaintext
NOT NULL constraint failed: challenge_record.creator_id
```

That's because now you demand a creator in your model, but you didn't provide it. You don't want to add it via form, as you did it for other data. You want for the creator to be automatically added, based on which user posted the new record. You can hook on the objects save method in your view and overwrite it, so creator = user.

```python
# challenge/views.py
# imports here

class RecordAdd(generics.CreateAPIView):
    serializer_class = RecordSerializer

    def perform_create(self, serializer):
        serializer.save(creator=self.request.user)
```

Try to create a record, logged in as another user again and you should see no error. Now, go check the records admin panel at http://127.0.0.1:8000/admin/challenge/record/ and you can see that you created another record with a different user.

![list_records_admin.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368635830/-veN6Szyj.png align="left")

## List all users with their records

You covered the posting-the-record part. Now move to the second part of your requirements - getting the data for all the competitors. Before you proceed and make things complicated, start simple.

First, add a serializer *CompareSerializer* inside "challenge/serializers.py\*:

```python
# challenge/serializers.py
# imports and RecordSerializer here

class CompareSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("__all__")
```

You retrieve all the data that you have on each user.

Next, add a new view inside "challenge/views.py":

```python
# challenge/views.py
# imports and RecordAdd here
from .serializers import RecordSerializer, CompareSerializer
from django.contrib.auth.models import User


class CompareView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = CompareSerializer
```

There are some differences with the `RecordAdd` view:

1. Instead of `CreateAPIView`, you use `ListAPIView` - that's because you don't want to add a new record but to see all the records.
    
2. You didn't need the queryset for `RecordAdd`, because you didn't do anything with previous records. Now you're showing all the records, so you need to retrieve them.
    
3. You used the `CompareSerializer` instead of `RecordSerializer`
    

After that, add an url to "challenge/urls.py":

```python
# challenge/urls.py
# imports here

urlpatterns = [
    path('add-record', RecordAdd.as_view()),
    path('compare', CompareView.as_view())
]
```

Now navigate to http://127.0.0.1:8000/compare and ... - ok, it's working but it's not the data you need.

## SerializerMethodFields

`SerializerMethodField` is a read-only field that gets its value by calling a method on its Serializer. You can use it to add any sort of data to your serialized object.

Edit your `CompareSerializer`:

```python
# challenge/serializers.py
from django.db.models import Sum
# other imports and RecordSerializer

class CompareSerializer(serializers.ModelSerializer):
    pushups = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = ("username", "pushups")

    def get_pushups(self, obj):
        return obj.records.aggregate(Sum('pushups'))['pushups__sum']
```

`SerializerMethodField` and its method are automatically connected with a DRF method `get_<field_name>`. That means that the belonging method has the same name as the field, just prefixed with `get_`. You need to add the `SerializerMethodField` to the `Serializer`s fields.

Now for the method: `obj` refers to the instance of `Model` you're using for your `Serializer`. `.aggregate` is a Django's function that summarizes a collection of objects and with `Sum`, you calculate their sum. `obj.records.aggregate(Sum('pushups'))`returns a dictionary, so you add `['pushups__sum']` to access just the desired key.

Refresh http://127.0.0.1:8000/compare and check if it works.

![pushups_sum.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368651362/wVtt-0YL9.png align="left")

You got the count of pushups! To provide all the data you wanted, you just need to do the same for the squats.

```python
class CompareSerializer(serializers.ModelSerializer):
    pushups = serializers.SerializerMethodField()
    squats = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = ("username", "pushups", "squats")

    def get_pushups(self, obj):
        return obj.records.aggregate(Sum('pushups'))['pushups__sum']

    def get_squats(self, obj):
        return obj.records.aggregate(Sum('squats'))['squats__sum']
```

You added another `SerializerMethodFIeld` and its method. Don't forget to add this method field to the list of fields inside the `Meta` class. Refresh the page and the second `APIView` is completed!

![pushups_squats_count.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368664447/SCPXmD6OP.png align="left")

## Limit the API solely to authenticated users

You don't want the data of your users to be seen by anyone who just wanders around your API. You want to limit the access to this API point. There are 3 options for limiting the access:

* Project-level (restrict the access to the whole API)
    
* View-level (restrict the access to whole API View)
    
* Object-level (restrict the access only for part of the view)
    

### Default permission policy

Currently, you don't want someone that is not registered, to see anything. You'll restrict access to your API globally. Edit "tutorial/setting.py" and add this at the bottom:

```python
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}
```

With this, you allow access to your API only to authenticated users. Logout from the API and you'll see this:

![access_restricted.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368682259/X-_b4OwtL.png align="left")

Even if you navigate to */add-record*, the message you will be the same.

Remember how to do that, but for now, delete the code you added in the `settings.py` file (`REST_FRAMEWORK ... IsAuthenticated',]}`). You need to learn about another possibility.

### View level permission

Let's say the family and friends of the competitors want to monitor the competition. You want them to see the list of competitors, but you don't want them to add the records. If they'd try, they'd get a nasty error:

![not_authenticated_error.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368696230/qqNuSPzbz.png align="left")

You don't want that. You want to limit the access for the `add-record` endpoint.

Edit the `RecordAdd` class in "challenge/views.py":

```python
# challenge/views.py

from rest_framework.permissions import IsAuthenticated # don't forget the import!

class RecordAdd(generics.CreateAPIView):
    permission_classes = [IsAuthenticated]

    serializer_class = RecordSerializer

    # ...
```

Open http://127.0.0.1:8000/add-record. You're logged out, so this is the view you get to see:

![add_restricted.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368716644/54unt1pDi.png align="left")

Now navigate to http://127.0.0.1:8000/compare. Even when you're logged out, your view is the same as for an authenticated user:

![compare_open.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368730103/FoCfMTjO8.png align="left")

If you log in and go back to */add-record*, you'll be able to post a new record:

![logged_in.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368744756/1rq31VTXN.png align="left")

> Check all the possibilities of the restrictions [here](https://www.django-rest-framework.org/api-guide/permissions/#api-reference).

## Root API Endpoint

You can make your API easier to browse. On your root page(`/`), you want to make a list of all the endpoints.

For that, you need to create another view and connect it with the root url.

For this, you don't need Serializer, you'll use just a simple Django function-based view.

Add this at the top of the "challenge/views.py"e:

```python
# challenge/views.py
# ... imports here
@api_view()
def api_root(request, format=None):
    return Response({
        'add': reverse('add-record', request=request, format=format),
        'compare': reverse('compare', request=request, format=format)
    })

# ... RecordAdd and CompareView here
```

Django receives an instance of `HttpRequest` and returns `HttpResponse`, but DRF receives an instance of `Request` and returns `Response`. To change a Django view to a DRF view, use the `@api_view` decorator. You provide both API points in the `Response`. `reverse` returns a fully qualified URL, using the request to determine the host and port. That way, the urls are correct, either on your localhost or in the production, without the need to change anything in the code.

`add-record` inside the `reverse` method corresponds to the name of the url. You don't have that in your "challenge/urls.py". Add it:

```python
# challenge/urls.py

from django.urls import path

from .views import RecordAdd, CreatorList, CompareView, api_root

urlpatterns = [
    path('add-record', RecordAdd.as_view(), name='add-record'),
    path('compare', CompareView.as_view(), name='compare'),
    path('', api_root)
]
```

You imported the `api_root` view you wrote a few minutes back and added it to the root of your website. To the paths `add-record` and `compare` you added names, so the `reverse` function in `api_root` can find them.

Open http://127.0.0.1:8000/ and check it yourself - it works!

![api_root.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1615368759432/b88nX5aQg.png align="left")

You can also click on links of `add` and `compare` and that makes it easy to navigate through your API.

## Conclusion

You're now able to create a REST API with Django Rest Framework. You know how to create views that either allow you to post or to retrieve data. You know how to create serializers that change your `Model` data to `JSON`. You learned how to restrict access and how to make your API browsable. You also learned how to change the data you get from the `Models` and serve the change data on your API.

You can play around with it a little more:

* create an endpoint that will output the current winner
    

create an endpoint where only an owner will be able to see and change/delete their record

If you're looking for more advanced content, you can check the articles on DRF that I wrote on Testdriven.io:  
\- [**Django REST Framework Views series**](https://testdriven.io/blog/drf-views-part-1/)  
**\-** [**Permissions in Django REST Framework series**](https://testdriven.io/blog/drf-permissions/)

<sub>Image by slightly_different on Pixabay</sub>
