Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing HATEOAS in Django REST framework

I'm trying to implement REST API that implements HATEOAS using Django REST Framework (DRF). I know that DRF itself doesn't support HATEOAS and I didn't find any examples of such implementation. Therefore I'm not sure on which level of DRF (Serializers / Views / Renderers) should I implement this functionality. Do you have some experiences, thoughts, insights or examples which could help me to start? Thank you.

like image 451
snakey Avatar asked Nov 26 '15 17:11

snakey


People also ask

What are the advantages of HATEOAS for restful API?

Using HATEOAS allows an API to clearly define a control logic that is available at the client-side. This enables them to follow links embedded in the API resources instead of having them manipulate URLs. This decouples clients from the URL structure so that changes don't end up hurting integration.

What does HATEOAS stand for?

Hypermedia as the Engine of Application State (HATEOAS) is a constraint of the REST application architecture that distinguishes it from other network application architectures. With HATEOAS, a client interacts with a network application whose application servers provide information dynamically through hypermedia.

What is hypermedia in REST?

Hypermedia is an important aspect of REST. It lets you build services that decouple client and server to a large extent and let them evolve independently. The representations returned for REST resources contain not only data but also links to related resources.


1 Answers

Here is my solution implemented on level of Viewset:

from rest_framework import viewsets
from rest_framework import generics
from serializers import EventSerializer, BandSerializer
from rest_framework.response import Response
from rest_framework import status

from collections import OrderedDict

class LinksAwarePageNumberPagination(PageNumberPagination):
   def get_paginated_response(self, data, links=[]):
       return Response(OrderedDict([
          ('count', self.page.paginator.count),
          ('next', self.get_next_link()),
          ('previous', self.get_previous_link()),
          ('results', data),
          ('_links', links),
       ]))

class HateoasModelViewSet(viewsets.ModelViewSet):
    """
    This class should be inherited by viewsets that wants to provide hateoas links
    You should override following methodes:
      - get_list_links
      - get_retrieve_links
      - get_create_links
      - get_update_links
      - get_destroy_links
    """

    pagination_class = LinksAwarePageNumberPagination


    def get_list_links(self, request):
        return {}

    def get_retrieve_links(self, request, instance):
        return {}

    def get_create_links(self, request):
        return {}

    def get_update_links(self, request, instance):
        return {}

    def get_destroy_links(self, request, instance):
        return {}

    def get_paginated_response(self, data, links=None):
        assert self.paginator is not None
        return self.paginator.get_paginated_response(data, links)

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data, links=self.get_list_links())

        serializer = self.get_serializer(queryset, many=True)

        return Response(OrderedDict([
            ('results', serializer.data),
            ('_links', self.get_list_links(request))
        ]))

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        data = serializer.data
        data['_links'] = self.get_retrieve_links(request, instance)
        return Response(data)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        data = serializer.data
        data['_links'] = self.get_create_links(request)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)
        data = serializer.data
        data['_links'] = self.get_update_links(request, instance)
        return Response(serializer.data)

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        data = {'_links': self.get_destroy_links(request, instance)}
        self.perform_destroy(instance)
        return Response(data, status=status.HTTP_204_NO_CONTENT)

Example of usage:

class EventViewSet(HateoasModelViewSet):
    queryset = Event.objects.all()
    serializer_class = EventSerializer

    def get_list_links(self, request):
        return {
            'self': {'href': request.build_absolute_uri(request.path)},
            'related_link1': {'href': '...'},
        }
like image 138
snakey Avatar answered Oct 07 '22 21:10

snakey