Methods from a single view to different endpoints: keep APIs for HTML and for JSON separated (Django Rest Framework)
Could you help suggesting how to keep coding style organised to keep endpoints from HTML and JSON separated, in Django Rest Framework ?
In Flask I am used to keeps endpoints for serving Json, and ones for serving HTML, separated, like:
@application.route('/api/')
def api_root():
#...
return jsonify({'data' : data})
and
@application.route('/home/<string:page>/', endpoint='page_template')
#...
return render_template(template, page)
And so I could serve the APIs like:
/api/page => serve the json for the page, say for AJAX etc.
/page => serve the corresponding html page
In Django RF, I read that a ModelViewSet can serve both.
So I could keep everything in one place.
However, when I come to map views on the router, I would have all the endpoint served respect the path related my model, they would be all sub-path of /api
Could you help in advising a good coding practice to make use of ModelViewSet, and route endpoints for html separated from APIs ?
This is the example Im working on, my doubts are in comments:
from rest_framework import viewsets
from rest_framework import generics
from rest_framework.decorators import action
from rest_framework.response import Response
from .serializers import PersonSerializer
from .models import Person
class PersonViewSet( viewsets.ModelViewSet):
queryset = Person.objects.all().order_by('name')
serializer_class = PersonSerializer
# this will return last person
# I can see it registered at: 127.0.0.1:8000/api/people/last_person/
@action(detail=False)
def last_person(self, request):
queryset = Person.objects.all().order_by('timestamp').reverse()[0]
serializer = self.get_serializer(queryset)
return Response(serializer.data)
# this will return a template:
# I can see it registered at: ../api/people/greetings : I wanted at /greetings
@action(detail=False)
def greetings(self, request):
queryset = Person.objects.all().order_by('timestamp').reverse()[0]
serializer = self.get_serializer(queryset)
return render(
request,
'myapi/greetings.html',
{
'person': serializer.data
}
)
Also, please note how I am serving the method greetings
: here I am repeating the queryset and serialising part. I thought to do:
def greetings(self, request):
person = self.last_person(request)
return render(
request,
'myapi/greetings.html',
{
'person': person
}
)
But it will give error, because person
would be a Response
object, and could not find a way to pass it to the render
.
Which could be a good coding style to avoid replicating things, and keep APIs and templates separated ?
In /myapi/url.py
I am registered the endpoints like:
router = routers.DefaultRouter()
router.register(r'people', views.PersonViewSet)
app_name = 'myapi'
urlpatterns = [
path('', include(router.urls)),
]
In the main url.py
, like this:
from django.urls import include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('myapi.urls')),
path('', include('myapi.urls')) # How to keep views for templates and for Json separated ??
]
Answer
To organize your Django project with separate HTML and JSON endpoints while using Django Rest Framework (DRF), you can structure the views and routing in a way that maintains clear separation. Here's a suggested approach based on your example:
1. Separate Views for API and HTML
You can still use Django views for rendering HTML templates and DRF viewsets for serving JSON. The main idea is to separate the logic into distinct classes for serving HTML views and API endpoints.
You can organize your views as follows:
For JSON API Endpoints (using DRF ViewSets)
You can keep your ModelViewSet
for serving JSON, as it handles CRUD operations and other custom actions like last_person
efficiently.
# myapi/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .serializers import PersonSerializer
from .models import Person
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all().order_by('name')
serializer_class = PersonSerializer
@action(detail=False)
def last_person(self, request):
person = Person.objects.all().order_by('timestamp').reverse().first()
serializer = self.get_serializer(person)
return Response(serializer.data)
In your urls.py
, register this viewset under the /api/
route.
# myapi/urls.py
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'people', views.PersonViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
For HTML Views (using Django Class-Based Views or Function-Based Views)
For rendering HTML templates, you can define a class-based view or function-based view to serve the HTML templates.
# myapi/views.py
from django.shortcuts import render
from django.views import View
from .models import Person
class PersonGreetingView(View):
def get(self, request):
# Get the last person from the database
person = Person.objects.all().order_by('timestamp').reverse().first()
return render(request, 'myapi/greetings.html', {'person': person})
In your urls.py
, add a route for rendering the HTML:
# myapi/urls.py
from . import views
urlpatterns = [
path('greetings/', views.PersonGreetingView.as_view(), name='greetings'),
]
2. Keeping HTML and JSON Routes Separated
To keep your JSON API and HTML routes separated, you can use distinct URL namespaces. The DRF routes will stay under /api/
and your HTML views can be served under /
or another path.
Main urls.py
In your main urls.py
, include both API and template routes. Notice how api/
is reserved for JSON and myapi/
can be for your HTML views.
# main/urls.py
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('myapi.urls')), # API JSON endpoints
path('', include('myapi.urls')), # HTML views
]
In the above example, api/
serves the JSON endpoints, and the default root serves HTML views.
3. Avoid Repeating Queryset Logic for HTML and API
To avoid repeating the logic for fetching the same data (e.g., the last person), you can factor out the common logic into a utility method or a method in your ModelViewSet
and View
. This will keep things DRY (Don't Repeat Yourself).
Refactor Common Logic
# myapi/views.py
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Person
from .serializers import PersonSerializer
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all().order_by('name')
serializer_class = PersonSerializer
def get_last_person(self):
return Person.objects.all().order_by('timestamp').reverse().first()
@action(detail=False)
def last_person(self, request):
person = self.get_last_person()
serializer = self.get_serializer(person)
return Response(serializer.data)
class PersonGreetingView(View):
def get(self, request):
person = PersonViewSet().get_last_person()
return render(request, 'myapi/greetings.html', {'person': person})
This approach ensures that your logic for fetching the last_person
is reused without repeating the database query in both the API and HTML views.
4. Summary
- Separation of Concerns: Keep your API views (JSON endpoints) separate from your HTML views using Django's view system and DRF's viewsets.
- URLs: Use different URL prefixes (e.g.,
/api/
for JSON and/
or/myapp/
for HTML) to keep routes organized. - Common Logic: Factor out any common logic (like fetching the last person) into reusable methods or functions to avoid repeating code.
By using this structure, you maintain a clean separation between serving HTML and JSON content, while also making the code more maintainable and organized.