Getting Started
Part 1, Chapter 3
In this chapter, you'll set up the base project structure, wire up DRF, and create your first API endpoint. You'll see a simple example of how the main DRF components -- serializers, views, and routers -- work together.
An endpoint is simply a point of entry for communicating with your API.
Initial Setup
Let's start working on the shopping list project.
First, create a new project and initialize a git repo:
$ mkdir drf-shopping && cd drf-shopping
$ git init
$ git checkout -b main
To avoid adding unwanted files to git, add a .gitignore file. Then, populate it from the .gitignore file here.
Add the .gitignore file to git, and create a commit:
$ git add .gitignore
$ git commit -m 'Initial commit'
Next, sign up for a GitHub account if you don't already have one. Add a new repo for your project, making sure to add the remote origin
repo to your local repo.
Push your changes from your local repository to your new remote repository:
$ git push -u origin main
Having trouble setting up your remote GitHub repo? Review How to Push to GitHub.
Next, let's create a new Django project:
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$ pip install Django==5.0.6 djangorestframework==3.15.1
(venv)$ django-admin startproject core .
(venv)$ django-admin startapp shopping_list
Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.
So, we created a Django project called core
with an app called shopping_list
that we'll use for creating our API.
Include both the shopping_list
app that you just created and DRF in your INSTALLED_APPS:
# core/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"shopping_list",
]
Model
As mentioned in the course introduction, models often correspond with the resources. Our resource is shopping item(s).
Let's create a new model called ShoppingItem
in shopping_list/models.py with name
, purchased
, and id
fields:
# shopping_list/models.py
import uuid
from django.db import models
class ShoppingItem(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
purchased = models.BooleanField()
def __str__(self):
return f"{self.name}"
Although Django gives each model an auto-incrementing primary key ID by default, we defined our own ID using Django's UUIDField to help prevent people from easily accessing records.
Create and apply the migrations:
(venv)$ python manage.py makemigrations shopping_list
(venv)$ python manage.py migrate
Next, create a superuser, so you'll be able to access the Django admin:
(venv)$ python manage.py createsuperuser
Register the model in shopping_list/admin.py:
# shopping_list/admin.py
from django.contrib import admin
from shopping_list.models import ShoppingItem
admin.site.register(ShoppingItem)
Serializers
Why do I need a serializer?
While working with Django, you tend to use complex data types and structures, like model instances. Since those are specific to Django, a client wouldn't know what to do with it. So, complex, Django-specific data structures need to be converted into something less complex that a client knows how to work with. That's what serializers are for. They convert complex data structures to native Python data types. Native data types can then be easily converted to content types, like JSON and XML, that other computers or systems can read and understand:
Django QuerySets -> Python dictionaries -> JSON
This also happens vice versa: Parsed data is deserialized into complex data types:
JSON -> Python dictionaries -> Django QuerySets
While deserializing the data, serializers also perform validation.
Generally, you write your serializers in a serializers.py file. If it becomes too big, you can restructure it into a separate Python package.
DRF comes with a few serializer types out-of-the-box:
- BaseSerializer
- ModelSerializer
- HyperlinkedModelSerializer
- ListSerializer
If your serializer maps closely to your model, it's best to use ModelSerializer.
It's a good idea to always start with the ModelSerializer. Only deviate from it if you have a good reason for not using it.
In this course, we'll keep our API-related code out of the core application in a Python package called api
. This should help keep API-related code in a consistent location, separate from the general app-related code.
A Python package is a folder that includes an (empty) __init__.py file.
So, create a folder called "api" within "shopping_list". Then, within "api", add an __init__.py file along with a serializers.py file:
api
├── __init__.py
└── serializers.py
Your entire project structure should now look like this:
├── .gitignore
├── core
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
├── manage.py
└── shopping_list
├── __init__.py
├── admin.py
├── api
│ ├── __init__.py
│ └── serializers.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── __init__.py
├── models.py
├── tests.py
└── views.py
Add a ShoppingItemSerializer
to shopping_list/api/serializers.py:
# shopping_list/api/serializers.py
from rest_framework import serializers
from shopping_list.models import ShoppingItem
class ShoppingItemSerializer(serializers.ModelSerializer):
class Meta:
model = ShoppingItem
fields = ["id", "name", "purchased"]
To declare a model serializer, we need to provide a model
and list of fields
in a Meta
subclass.
The ModelSerializer
class automatically created the serializer fields and validators from the corresponding model's fields from ShoppingItem
.
For example, you can see here that ShoppingItemSerializer
has a UniqueValidator for the id
field:
(venv)$ python manage.py shell
>>> from shopping_list.api.serializers import ShoppingItemSerializer
>>> serializer = ShoppingItemSerializer()
>>> print(repr(serializer))
ShoppingItemSerializer():
id = UUIDField(required=False, validators=[<UniqueValidator(queryset=ShoppingItem.objects.all())>])
name = CharField(max_length=100)
purchased = BooleanField()
>>>
View
Just like with regular Django applications, DRF also uses views. With DRF views, since you're returning machine readable data, like JSON or XML, you don't need to configure a template.
DRF views are a complex subject. You can read a detailed explanation of all the different views in my Django REST Framework Views Series.
The most widely used views are:
Name Description (Dis)advantages APIView The base class for every other view class Most freedom, most code to write Generic views Provides out-of-the-box endpoints (e.g., ListCreateAPIView) Some freedom, some code to write (ReadOnly)ModelViewSet Creates both list and detail endpoints Little freedom, little code to write
Among all the possibilities, ModelViewSet has the most going on under the hood, so it's the easiest to set up.
ViewSets allow you to build the CRUD operations around the same resource in a clean, straightforward manner. They cover retrieving a collection and adding a new resource to it along with retrieving, updating, and deleting a single resource with a minimal amount of code.
Now, since ViewSets are so much different from other types of views, it's a good practice to add them to a separate file called viewsets.py. Add this file to "shopping_list/api":
# shopping_list/api/viewsets.py
from rest_framework.viewsets import ModelViewSet
from shopping_list.api.serializers import ShoppingItemSerializer
from shopping_list.models import ShoppingItem
class ShoppingItemViewSet(ModelViewSet):
queryset = ShoppingItem.objects.all()
serializer_class = ShoppingItemSerializer
Here, we provided the queryset
and serializer_class
attributes, which will serve up all shopping list items.
Like ModelSerializer, ModelViewSet is useful when your view maps closely to the model. This is perfect for now since we don't have any additional complicated logic in the view.
URL
Finally, create a shopping_list/urls.py file and include it in your project's urls.py file:
# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("shopping_list.urls")),
]
Next, we need to create a router that will automatically generate the URL configurations for the ViewSet.
APIView
and generic views do not require a router. With those views, you create the URL patterns in the same manner that you're used to with Django.
DRF provides two possible routers:
Since the DefaultRouter includes a default API root view, making it easier to navigate the Browsable API, we'll use this one.
Wire up the app-specific URLs and routers in shopping_list/urls.py:
# shopping_list/urls.py
from django.urls import path, include
from rest_framework import routers
from shopping_list.api.viewsets import ShoppingItemViewSet
router = routers.DefaultRouter()
router.register("shopping-items", ShoppingItemViewSet, basename="shopping-items")
urlpatterns = [
path("api/", include(router.urls)),
]
After creating a new DefaultRouter
instance, we registered ShoppingItemViewSet
to it. We then included the router with the URL patterns.
You can register multiple ViewSets to the same router.
The register()
method has three arguments:
prefix
(required) - URL prefix for the ViewSetviewset
(required) - The ViewSet classbasename
(optional) - The base to use for the URL names
While the
basename
is generated by default from the QuerySet, it's a good idea to be explicit and define it for clarity purposes.
A router produces URLs in a RESTful manner:
- URL for creating a new resource and for retrieving the collection:
http://127.0.0.1:8000/api/shopping-items
. - URL for retrieving, updating, and deleting a single resource. Example:
http://127.0.0.1:8000/api/shopping-items/137c4523-3123-486d-981b-bcf0fd391d10/
(replace the UUID portion of the URL with your own UUID).
It's worth noting that you can also append the router-generated URLs to existing
urlpatterns
like so:urlpatterns += router.urls
.
It's a good practice to prepend all of your API endpoints with api/
to separate the API endpoints from other URLs.
To test, run the Django development server:
(venv)$ python manage.py runserver
You should be able to view your newly created endpoint at http://127.0.0.1:8000/api/shopping-items/. We'll look at how to interact with the endpoint in the next chapter.
Commit your changes and push them to GitHub:
(venv)$ git add -A
(venv)$ git commit -m 'Simple shopping list'
(venv)$ git push -u origin main
Summary
In this chapter, you used a serializer to convert complex data types to native ones. Since both our serializer and view map closely back to the model, we used a ModelSerializer and a ModelViewSet for our serializer and view, respectively. Finally, we wired up a router to automatically generate the URL patterns.
In the next chapter, we'll look at how to check our work by manually testing out the endpoint.
--
├── .gitignore
├── core
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
├── manage.py
└── shopping_list
├── __init__.py
├── admin.py
├── api
│ ├── __init__.py
│ ├── serializers.py
│ └── viewsets.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── __init__.py
├── models.py
├── tests.py
├── urls.py
└── views.py
✓ Mark as Completed