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:

  1. BaseSerializer
  2. ModelSerializer
  3. HyperlinkedModelSerializer
  4. 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:

  1. SimpleRouter
  2. DefaultRouter

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:

  1. prefix (required) - URL prefix for the ViewSet
  2. viewset (required) - The ViewSet class
  3. basename (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:

  1. URL for creating a new resource and for retrieving the collection: http://127.0.0.1:8000/api/shopping-items.
  2. 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