Making your life (h)APIer
with Django

By Emmanuelle Delescolle

Who am I?

API?

In computer programming, an application programming interface (API) is [...] a set of clearly defined methods of communication among various components.

 

A good API makes it easier to develop a computer program by providing all the building blocks, which are then put together by the programmer.

 

An API may be for a web-based system, operating system, database system, computer hardware, or software library.

Source: Wikipedia

REST API!

Endpoint base (/api/products/):

  • GET => list all
  • POST => create

Item (/api/products/3/):

  • GET => retrieve item
  • PUT => update all fields
  • PATCH => update some fields
  • DELETE => delete item

Discoverable!

Why do we need REST  API's
with Django projects

Searchable Dropdown

Web Frontend (Ember, Vue, React, ...)

Full-fledged client (GTK, mobile app, Java, ...)

Making data available to your users

Because we have proven many times the concept works

Also...

Django REST Framework

Has become the de-facto standard for API's in the Django eco-system.

Tastypie?

Had some success in the beginning, less so now.

Similar to other frameworks like Rails.
Its success was partially due to its fast prototyping abilities.

API Endpoint with DRF

from restframework import viewsets, serializers

from .model import Product, Category


class ProductSerializer(serializers.ModelSerializer):

    class Meta:
        model = Product
        fields = ('id', 'name', 'category', 'in-stock')

class ProductViewSet(viewsets.ModelViewSet):

    serializer_class = ProductSerializer
    queryset = Product.objects.all()


class CategorySerializer(serializers.ModelSerializer):

    class Meta:
        model = Category
        fields = ('id', 'name', 'products')

class CategoryViewSet(viewsets.ModelViewSet):

    serializer_class = CategorySerializer
    queryset = Category.objects.all()
from django.conf.urls import url, include
from rest_framework import routers
from catalog.api import ProductViewSet, CategoryViewSet


router  routers.DefaultRouter()

router.register(ProductViewSet)
router.register(CategoryViewSet)

urlpatterns = [
    ...
    url(r'^api/', include(router.urls)),
]

API Endpoint with Tastypie

from tastypie.resources import ModelResource
from .models import Product, Category


class ProductResource(ModelResource):
    class Meta:
        queryset = Product.objects.all()
        resource_name = 'product'


class CategoryResource(ModelResource):
    class Meta:
        queryset = Category.objects.all()
        resource_name = 'category'
from django.conf.urls import url, include
from tastypie.api import Api
from myapp.api import ProductResource, CategoryResource

api = Api(api_name='myapi')
api.register(UserResource())
api.register(EntryResource())

urlpatterns = [
    ...
    url(r'^api/', include(api.urls)),
]

One Day...

But...

💖 Django Admin 💖

from django.contrib import admin

from .models import Product, Category


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    pass


admin.site.register(Category)
from django.conf.urls import include, url
from django.contrib import admin


urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    ...
]
from drf_auto_endpoint.router import router, register
from drf_auto_endpoint.endpoints import Endpoint

from .models import Product, Category


@register
class ProductEndpoint(Endpoint):
    model = Product


router.register(Category)

API Endpoint with DRF +

DRF-Schema-Adapter

from django.conf.urls import include, url
from drf_auto_endpoint.router import router


urlpatterns = [
    url(r'^/api/', include(router.urls)),
    ...
]

Fast prototyping/concise code

with the Endpoint class

from drf_auto_endpoint.endpoints import Endpoint

from .models import Product


class ProductEndpoint(Endpoint):
    
    model = Product
    filter_fields = ('category_id', )
    search_fields = ('name', )
    ordering_fields = ('in_stock', )
from drf_auto_endpoint import Endpoint
from rest_framework import viewsets

from .models import Product
from .serializers import ProductSerializer


class ProductViewSet(viewsets.ModelViewSet):

    def create(self, request, *args, **kwargs):
        # do something fancy
        return super(ProductViewSet, self) \
            .create(request, *args, **kwargs)


class ProductEndpoint(Endpoint):
    
    model = Product
    base_viewset = ProductViewSet
    base_serializer = ProductSerializer

Fast prototyping/concise code

with factories

from rest_framework import serializers
from drf_auto_endpoint.factories import serializer_factory

from .models import Category


class ProductSerializer(serializers.ModelSerializer):

    category = serializer_factory(
        model=Category,
        ['id', 'name', '__str__']
    )(read_only=True)

Fast prototyping/concise code

with the Router and auto-discovery

from drf_auto_endpoint.endpoint import Endpoint
from drf_auto_endpoint.router import register, router

from .models import Product, Category


@register
class ProductEndpoint(Endpoint):
    
    model = Product
    filter_fields = ('category_id', )
    search_fields = ('name', )
    ordering_fields = ('in_stock', )


router.register(Category)

catalog/endpoints.py

from django.conf.urls import include, url

from drf_auto_endpoint.router import router


urlpatterns = [
    ...
    url(r'^api/', include(router.urls)),
]

project/urls.py

INSTALLED_APPS = (
    ...
    'drf_auto_endpoint',
)

settings.py

BUT...

What about being discoverable?

Hyperlinked relationships

DRF's Metadata classes

Disclaimer

Can we get more information from our endpoints?

REST_FRAMEWORK = {
    'DEFAULT_METADATA_CLASS': \
        'drf_auto_endpoint.metadata.AutoMetadata',
}

DRF_AUTO_METADATA_ADAPTER = \
    'drf_auto_endpoint.adapters.EmberAdapter'

settings.py

Defining and exposing extra methods?

    custom_actions = [
        {
            'type': 'modelMethod',
            'method': 'makeDraft',
            'icon_class': 'fa fa-fire',
            'btn_class': 'btn btn-warning',
            'text': 'Make draft',
            'display_condition': {
                'operator': 'eq',
                'value': 'open',
                'property_path': 'state',
            },
        }
    ]

    @custom_action(method='POST', icon_class='fa fa-money', btn_class='btn btn-success', text='Pay',
                   pushPayload=True, allowBulk=True,
                   display_condition={'operator': 'eq', 'value': 'open', 'property_path': 'state', })
    def pay(self, request, pk):
        obj = get_object_or_404(self.model, pk=pk)
        obj.state = 'paid'
        obj.save()
        serializer = self.get_serializer(obj)
        rv = {}
        rv[self.get_url()] = [serializer.data, ]
        return Response(rv)

Building custom adapters

  • BaseAdapter (JSON Schema)
  • AngularFormlyAdapter
  • ReactJsonSchemaAdapter
  • EmberAdapter
  • <insert your adapter>

Exporting serializers to client "models"?

Complex example in the wild

More?

Come find me in the hallways or during the sprints!

Questions

Links

Making your life (h)APIer with Django

By Emma

Making your life (h)APIer with Django

DjangoCon Europe 2019 talk

  • 2,245