Cookbook: creating endpoints from models
Setup
In this cookbook, we will see how to create endpoints from models without building any frontend yet.
First let's kickstart our project by installing cc_project_app_drf which will setup a Django project with a virtualenv and Django REST Framework pre-installed.
To start with this cookiecutter, you will first need to install cookiecutter globally on your computer if you haven't already done so.
sudo pip install cookiecutter
Once cookiecutter is installed you can go forward and bootstrap your new project:
$ cookiecutter https://bitbucket.org/levit_scs/cc_project_app_drf.git
project_name [Project name]: Simple test
repo_name [simple_test]:
author [Your Name]: Emma
username [emma]:
email [you@domain.com]: emma@example.com
python [/usr/bin/python3.6]:
create_superuser [no]: yes
you can now go to the newly created folder (simple_test
in my case), activate
your virtualenv and launch Django dev server:
$ cd simple_test
$ source venv/bin/activate
$ ./manage.py runserver
After doing so, if you head to http://localhost:8000/api/v1/ you'll see that we have a running application with basic DRF setup and even already have an endpoint for users.
Before being able to use DRF-schema-adapter we'll now have to install it. In a new terminal window, activate your virtualenv and install drf-schema-adpater.
source venv/bin/activate
pip install drf-schema-adapter
And add drf_auto_endpoint
to your INSTALLED_APPS
.
## settings.py
...
INSTALLED_APPS = (
...
'drf_auto_endpoint',
)
drf_auto_endpoint
is one of the 2 modules provided by DRF-schema-adapter and
is responsible for generating endpoints.
Now that DRF-schema-adapter is installed, we can replace DRF's DefaultRouter
with drf_auto_endpoint
's router in the urls file.
With this cookiecutter, urls that are linked to the API are located in your
project's forlder in a file called api_urls.py
. In my case that would be
simple_test/api_urls.py
.
After the substitution, the file should look like this:
## simple_test/api_urls.py
from django.conf.urls import include, path
# from rest_framework import routers
from drf_auto_endpoint.router import router
from .views import UserViewSet
# router = routers.DefaultRouter()
#router.register(r'users', UserViewSet)
router.registerViewSet(r'users', UserViewSet)
urlpatterns = [
path('', include(router.urls)),
]
Creating endpoints from models
After doing this, you shouldn't notice any change in the output of DRF. The
only difference is that we are now using drf_auto_endpoint
's router which is
a subclass of DRF's DefaultRouter
.
It is now time to get started. In a virtualenv-activated shell, start a new application (let's call it catalog).
$ ./manage.py startapp catalog
## settings.py
INSTALLED_APPS = (
...
'catalog',
)
In this application, we will created two models: Category
and Product
.
## catalog/models.py
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=9, decimal_places=3)
Don't forget to create and run migrations for our new models.
$ ./manage.py makemigrations
$ ./manage.py migrate
Now that we have two new models, we are going to create endpoints for them.
Simple model endpoints are straight-forward to create. Models can be directly
registered on the router which will generate a ModelSerializer
and a
ModelViewSet
for you. Create new filecatalog/endpoints.py
with the following
content:
## catalog/endpoints.py
from drf_auto_endpoint.router import router
from .models import Category, Product
router.register(Category)
router.register(Product)
As endpoints.py
is a new file, you will have to restart your Django dev
server in order to see the changes.
Once this is done and you have reloaded
http://localhost:8000/api/v1/, you'll notice
that we now have two new endpoints available, one for each of our models.
At this point, you should probably create a couple categories and products from the DRF browable interface in order to be able to play with the interface later on.
This is great for prototyping but in real life our endpoints are rarely that simple. In the case of our application, let's say we want to use this application for an e-commerce. The frontend application that will connect to our api should not be allowed to write anything, it should also be able to perform search queries on products by name, filter queries by category_id and order results by name or price.
In order to achieve this we will have to create an Endpoint
class. As with
Django admin, instead of registering models directly on the router, you can use
the @register
decorator to register any Endpoint class on the router like this:
## catalog/endpoints.py
from drf_auto_endpoint.endpoints import Endpoint
from drf_auto_endpoint.router import router, register
from .models import Category, Product
@register
class ProductEndpoint(Endpoint):
model = Product
router.register(Category)
This is the same Endpoint class to which we can pass different parameters in order to customize it. For a full list of available parameters/attributes, please see the endpoint attributes section.
Customizing Endpoints
Let's implement the changes we mentioned above. As you'll notice, most
attributes are similar to attributes you would declare on a DRF ViewSet
.
## catalog/endpoints.py
from drf_auto_endpoint.endpoints import Endpoint
from drf_auto_endpoint.router import register
from .models import Category, Product
@register
class ProductEndpoint(Endpoint):
model = Product
read_only = True
search_fields = ('name', )
filter_fields = ('category_id', )
ordering_fields = ('price', 'name', )
@register
class CategoryEndpoint(Endpoint):
model = Category
read_only = True
Custom viewset
A common usecase when building an API is to have to slightly customize
ViewSet
's. An example of this is when you want the details view to perform a
slightly different operation than the list view like adding 1
to a counter.
Let's do that with our example app.
First we'll add a view counter to our Product model.
## catalog/models.py
...
class Product(models.Model):
...
views = models.PositiveIntegerField(default=0)
Then create and run the migrations.
./manage.py makemigrations
./manage.py migrate
You can notice that our new field is already available on the products endpoint without us having to do anything (after refreshing http://localhost:8000/api/v1/catalog/products/ ).
Now let's create a custom ViewSet.
## catalog/endpoints.py
from rest_framework import viewsets
from drf_auto_endpoint.endpoints import Endpoint
from drf_auto_endpoint.router import register
from .models import Category, Product
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
def retrieve(self, request, *args, **kwargs):
obj = self.get_object()
obj.views += 1
obj.save()
return super(ProductViewSet, self).retrieve(request, *args, **kwargs)
@register
class ProductEndpoint(Endpoint):
model = Product
read_only = True
search_fields = ('name', )
filter_fields = ('category_id', )
ordering_fields = ('price', 'name', )
base_viewset = ProductViewSet
...
As you might have noticed, we still don't have to specify any serializer_class
or queryset
parameter when creating the ViewSet, those will be added for us
by drf-schema-adapter.
If you have already created some products, go to http://localhost:8000/api/v1/catalog/products/1/ and refresh it a few times to see the counter go up. Of course, this is a naive implementation and a real-world scenario would be slightly more complex.
Customizing serializers
Now, intead of referencing a product's category by id like it is right now, let's say we want to embed the category information in the product records. For this we will need a custom serializer. Let's build one by only specifying the fields that should be different from the default serialiazer.
## catalog/endpoints.py
from rest_framework import viewsets, serializers
from drf_auto_endpoint.endpoints import Endpoint
from drf_auto_endpoint.router import register
from .models import Category, Product
@register
class CategoryEndpoint(Endpoint):
model = Category
read_only = True
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
def retrieve(self, request, *args, **kwargs):
obj = self.get_object()
obj.views += 1
obj.save()
return super(ProductViewSet, self).retrieve(request, *args, **kwargs)
class SimpleCategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = (
'id',
'name',
)
class ProductSerializer(serializers.ModelSerializer):
category = SimpleCategorySerializer()
@register
class ProductEndpoint(Endpoint):
model = Product
read_only = True
search_fields = ('name', )
filter_fields = ('category_id', )
ordering_fields = ('price', 'name', )
base_viewset = ProductViewSet
base_serializer = ProductSerializer
As you can see, again here, we only had to define whatever was not standard in
our serializer, we didn't have to declare a Meta
class as drf-schema-adapter
does this for us.
But wait... since we don't have to manually create a full seriliazer for
the Product
or Category
endpoints, it is a bit sad to have to create one
(very similar to the one used on the endpoint) for the nested Category
inside
the Product
endpoint!
Indeed and we don't have to create it manually we can use the same factory
function invoke by Endpooint
classes in order to create one for us.
## catalog/endpoints.py
from rest_framework import viewsets, serializers
from drf_auto_endpoint.endpoints import Endpoint
from drf_auto_endpoint.factories import serializer_factory
from drf_auto_endpoint.router import register
from .models import Category, Product
...
# class SimpleCategorySerializer(serializers.ModelSerializer):
#
# class Meta:
# model = Category
# fields = (
# 'id',
# 'name',
# )
class ProductSerializer(serializers.ModelSerializer):
category = serializer_factory(model=Category, fields=('id', 'name'))()
The call serializer_factory
function returns a serializer class for the
Category
model. It is therefore important to remember to instanciate the
value returned from that call (notice the ()
at the end of the call)