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.
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.
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.
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.
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': '...'},
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With