As you've seen so far Django is a framework that's set up to do a lot of the lower level work for you when creating a project to allow you to focus more abstract, higher level work. The Django REST Framework applies that same philosophy to the creation of RESTful APIs.
What is a RESTful API? Check out the following articles for more information:
Let's jump in and set up a simple API to see how it works!
We're going to be making a new Django project called ga
that will keep track
of General Assembly Students and Cohorts. Lets run mkdir ga
in our lessons
folder to give our project a home.
Next, let's set up our virtual environment and install the necessary dependencies.
- Create a new direcotry and cd into it
mkdir students-and-cohorts && cd students-and-cohorts
or if you've got Oh-My-Zsh
take students-and-cohorts
- Set up a virtual environment
pipenv shell
- Install the necessary dependencies
pipenv install django psycopg2-binary
- Last, lets use
django-admin startproject ga .
to have Django create our project. Don't forget the dot!
When we've created databases in the past, we've opened up psql
and entered our
commands there. Let's do something a little different this time, which is to
create a file that has our commands, then execute that file.
Create a file called create-database.sql
and fill it with the following:
CREATE USER ga_admin WITH PASSWORD 'password';
CREATE DATABASE ga WITH OWNER ga_admin;
We can execute that script by running psql -f create-database.sql
from the
command line. If you check the man
page for psql
, you'll see that the -f
option (or --file
) is used when you want to read commands from whatever file
is passed as the next argument.
The end result is identical to what would happen if you entered those commands inside the
psql
shell. The advantage of writing the script into a file is, of course, we can use it again! Normally on a project like this, we would delete thecreate-database.sql
at this point but you should keep (and maybe add a comment on how to execute the file) for reference sake.
Now that our database exists, let's configure Django to use our PostgreSQL database. That
setting is located in the settings.py
file that's located in our project
folder (ga/settings.py
). Look for the dictionary that looks like this:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
We're going to change our 'default'
database to point to the one we made in
psql
by altering the dictionary to look like this:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'ga',
'USER': 'ga_admin',
'PASSWORD': 'password',
'HOST': 'localhost'
}
}
Now let's run the server to see if we run into any issues. Normally we would
type out python manage.py runserver
to spin it up but that's so many letters
and we are so lazy. When working with Django, you'll find yourself typing
python manage.py
very often, which is a good indicator that you could benefit
from an alias.
If you like, you could throw the following alias into your zshrc/bashrc to save your hands some stress:
alias pm='python manage.py`
Don't forget to run source ~/.zshrc
afterward!
We're going to add an application called 'students' to our project. The first
step is to use django-admin startapp students
to create all of the necessary
files.
Then, we need to open the settings.py
file in our project folder and add
'students'
to the list of INSTALLED_APPS
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'students'
]
With that done, we can define our models in students/models.py
. We're going to
make a Cohort
model with a name and subject field and we're going to make a
Student
model with a name field that's going to be related to a Cohort
.
class Cohort(models.Model):
name = models.CharField(max_length=128)
subject = models.CharField(max_length=5)
class Student(models.Model):
name = models.CharField(max_length=128)
cohort = models.ForeignKey(
Cohort,
on_delete=models.CASCADE,
related_name='students'
)
A couple of notes:
- The
on_delete
argument is there to define specific behavior that should take place when an instance of aCohort
is deleted. We've provided the instructionmodels.CASCADE
. That means that when the referenced object is deleted, it will also delete the objects that have a reference to it.
Why? Imagine you had a blog post with a number of comments attached to it. If the blog post was deleted, you wouldn't want to keep the comments floating around attached to nothing, would you?
- The
related_name
is what name theStudent
model is given from the perspective of theCohort
.
Ok! Now that we've created some models in Django, we need to alter our SQL
database so that everything in synced up. That's what migrations are for! We
first create the migrations with python manage.py makemigrations
and
follow it with python manage.py migrate
to apply them.
We can limit the acceptable values for Cohort
subjects by passing in a
choices
argument to any of our fields. Choices is going to be a list of tuples
where the first value is how we'd like to store our choice in the database and
the second is what we'd like to be displayed in the admin panel.
Add the following above the model definitions in students/models.py
:
COHORT_SUBJECTS = [
('SEI', 'Software Engineering Immersive'),
('UXDI', 'User Experience Development Immersive'),
('DSI', 'Data Science Immersive'),
]
Then change the Cohort
model so that it looks like this:
class Cohort(models.Model):
name = models.CharField(max_length=128)
subject = models.CharField(max_length=5, choices=COHORT_SUBJECTS)
Because we've made a change to our model, we're going to need to make and run a
migration with python manage.py makemigrations
and python manage.py migrate
.
When you run the migration, you may get a response saying that it's impossible
to add a non-nullable field without specifying a default. You can add
default=None
to whatever field is giving you a problem like this:
subject = models.CharField(max_length=5, choices=COHORT_SUBJECTS,
default=None)
You can then delete that from your file and make and run a migration again and it won't give you any more trouble.
Next, lets register our models by jumping into students/admin.py
and editing
it to look like this:
from django.contrib import admin
from .models import Cohort, Student
# Register your models here.
admin.site.register(Cohort)
admin.site.register(Student)
Finally, we need to create our superuser. Run python manage.py createsuperuser
and run through the prompts.
Now let's load up localhost:8000/admin
in our browser and create some cohorts!
You may notice that once a cohort is created, it's listed in the admin panel as
"Cohort Object" rather than saying anything useful. That's because waaaaay
back in the day when we created our models, we didn't add any __str__
method
to describe what should be returned when we try to print our models.
If jump back into the students/models.py
file and add a nice definition...
def __str__(self):
return f'{self.subject} - {self.name}'
... then refresh the page, we get a nice formatted string. Note that we can do
any sort of valid python for the __str__
method, meaning we can make our
returns as simple or as fancy as we like. Go ahead and make a __str__
for the
Student
model too!
Now it's time to set up our serializers! From the Django REST documentation:
Serializers allow complex data such as querysets and model instances to be converted to native Python data types that can then be easily rendered into JSON, XML or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
But of course, if we want to use Django REST Framework, we need to install
it with pipenv install djangorestframework
.
Once that's done, we need to add Django REST to our INSTALLED_APPS
list in
ga/settings.py
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'students'
]
Next, we'll set up our serializers by creating serializers.py in students folder
from rest_framework import serializers
from .models import Cohort, Student
class CohortSerializer(serializers.ModelSerializer):
class Meta:
model = Cohort
fields = '__all__'
class StudentSerializer(serializers.ModelSerializer):
class Meta:
model = Student
fields = '__all__'
A view is essentially a function that gets called when a request comes in to
one of our endpoints. It's output is the information that we want to send
back. We can set that up inside of students/views.py
.
When you open the file, you'll see:
from django.shortcuts import render
This is Django's default method of creating views where the render
will take
a model and plug the information into an HTML template. We want to return JSON
so we're going to get rid of that and replace it with the viewsets
library
that comes from Django REST Framework:
from rest_framework import viewsets
from .serializers import CohortSerializer, StudentSerializer
from .models import Cohort, Student
# Create your views here.
class CohortViewSet(viewsets.ModelViewSet):
queryset = Cohort.objects.all()
serializer_class = CohortSerializer
class StudentViewSet(viewsets.ModelViewSet):
queryset = Student.objects.all()
serializer_class = StudentSerializer
We're importing our two serializers, importing the models for our data, then in
each view set we're extending the ModelViewSet
class that's come from
viewsets
. That class has lots of "under-the-hood" functionality that I
recommend checking out in the
documentation. Our
queryset
determines what information we're going to get from the database. The
serializer_class
says how we want that information to be managed.
Now that the views are configured, we need to say which view set is associated
with which endpoint. That's done inside of urls.py
. Normally, you would make a
URLs file inside of the each particular app in your project, then import them
into the URLs file in the project directory. But this time, we're just going to
define our URLs directly in the project folder's urls.py
(ga/urls.py
) just
to keep things simple. We can get away with this because our project only has a
single app.
Lets edit our ga/urls.py
to look like this:
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from students.views import CohortViewSet, StudentViewSet
router = routers.DefaultRouter()
router.register(r'cohort', CohortViewSet)
router.register(r'student', StudentViewSet)
urlpatterns = [
path('', include(router.urls)),
path('admin/', admin.site.urls)
]
This router takes the place of all the endpoints I'd have to write by hand
That's it! We can test it all out in Postman.
This is the start of the journey - feel free to view the Django REST Framework documentation.
Now let's build out our old friend the Cat Collector with a Django Rest Framework (and JWT) twist!
- Django Setup, URLs, and Views
- Models and Serializers
- One to Many Models
- Extra Rep
- Many to Many Models
- Authentication
and finally DRF + JWT Cat Collector Final Code
and FOR REAL finally Deploying on Heroku