Skip to main content
REST framework is suitable for returning both API-style responses and regular HTML pages. Additionally, serializers can be used as HTML forms and rendered in templates.

Rendering HTML

To return HTML responses, use either TemplateHTMLRenderer or StaticHTMLRenderer.

TemplateHTMLRenderer

TemplateHTMLRenderer renders data using Django templates. The response should contain a dictionary of context data. Basic Example:
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from myapp.models import Profile

class ProfileList(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    template_name = 'profile_list.html'

    def get(self, request):
        queryset = Profile.objects.all()
        return Response({'profiles': queryset})
Template (profile_list.html):
<html>
<body>
  <h1>Profiles</h1>
  <ul>
    {%% for profile in profiles %%}
      <li>{{ profile.name }} - {{ profile.email }}</li>
    {%% endfor %%}
  </ul>
</body>
</html>
Template Name Resolution: The template name is determined by (in order of preference):
  1. An explicit .template_name attribute on the response
  2. An explicit .template_name attribute on the renderer class
  3. The result of calling view.get_template_names()
class ProfileDetail(APIView):
    renderer_classes = [TemplateHTMLRenderer]

    def get(self, request, pk):
        profile = get_object_or_404(Profile, pk=pk)
        serializer = ProfileSerializer(profile)
        # Specify template on response
        return Response(
            {'serializer': serializer, 'profile': profile},
            template_name='profile_detail.html'
        )

StaticHTMLRenderer

StaticHTMLRenderer returns pre-rendered HTML content. The response should contain a string of HTML.
from rest_framework.renderers import StaticHTMLRenderer

class AboutView(APIView):
    renderer_classes = [StaticHTMLRenderer]

    def get(self, request):
        data = '<html><body><h1>About Us</h1></body></html>'
        return Response(data)
TemplateHTMLRenderer is for template-based HTML, while StaticHTMLRenderer is for pre-rendered HTML strings.

Mixed Renderers

Combine HTML and JSON renderers to support both content types:
from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer

class ProfileList(APIView):
    renderer_classes = [TemplateHTMLRenderer, JSONRenderer]
    template_name = 'profile_list.html'

    def get(self, request):
        queryset = Profile.objects.all()
        serializer = ProfileSerializer(queryset, many=True)
        return Response({'profiles': serializer.data})

# Access as HTML: GET /profiles/
# Access as JSON: GET /profiles/?format=json

Rendering Forms

Serializers can be rendered as forms using the render_form template tag.

Basic Form Rendering

View:
from django.shortcuts import get_object_or_404, redirect
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.views import APIView
from myapp.models import Profile
from myapp.serializers import ProfileSerializer

class ProfileDetail(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    template_name = 'profile_detail.html'

    def get(self, request, pk):
        profile = get_object_or_404(Profile, pk=pk)
        serializer = ProfileSerializer(profile)
        return Response({'serializer': serializer, 'profile': profile})

    def post(self, request, pk):
        profile = get_object_or_404(Profile, pk=pk)
        serializer = ProfileSerializer(profile, data=request.data)
        if not serializer.is_valid():
            return Response({'serializer': serializer, 'profile': profile})
        serializer.save()
        return redirect('profile-list')
Template:
{%% load rest_framework %%}

<html>
<body>
  <h1>Profile - {{ profile.name }}</h1>

  <form action="{%% url 'profile-detail' pk=profile.pk %%}" method="POST">
    {%% csrf_token %%}
    {%% render_form serializer %%}
    <input type="submit" value="Save">
  </form>
</body>
</html>
The render_form template tag automatically generates form fields based on your serializer definition.

Template Packs

REST framework includes three built-in template packs based on Bootstrap 3:
  • horizontal - Labels and controls side-by-side (2/10 column split)
  • vertical - Labels above controls (default)
  • inline - All controls inline

Including Bootstrap CSS

<head>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>

Vertical Template Pack

Labels appear above their corresponding control inputs (default):
{%% load rest_framework %%}

<form action="{%% url 'login' %%}" method="post" novalidate>
  {%% csrf_token %%}
  {%% render_form serializer template_pack='rest_framework/vertical' %%}
  <button type="submit" class="btn btn-default">Sign in</button>
</form>
Vertical form example

Horizontal Template Pack

Labels and controls side-by-side (used in browsable API):
{%% load rest_framework %%}

<form class="form-horizontal" action="{%% url 'login' %%}" method="post" novalidate>
  {%% csrf_token %%}
  {%% render_form serializer %%}
  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
      <button type="submit" class="btn btn-default">Sign in</button>
    </div>
  </div>
</form>
Horizontal form example

Inline Template Pack

Compact form with all controls inline:
{%% load rest_framework %%}

<form class="form-inline" action="{%% url 'login' %%}" method="post" novalidate>
  {%% csrf_token %%}
  {%% render_form serializer template_pack='rest_framework/inline' %%}
  <button type="submit" class="btn btn-default">Sign in</button>
</form>
Inline form example

Example Serializer

from rest_framework import serializers

class LoginSerializer(serializers.Serializer):
    email = serializers.EmailField(
        max_length=100,
        style={'placeholder': 'Email', 'autofocus': True}
    )
    password = serializers.CharField(
        max_length=100,
        style={'input_type': 'password', 'placeholder': 'Password'}
    )
    remember_me = serializers.BooleanField()

Field Styles

Customize field rendering using the style keyword argument:
details = serializers.CharField(
    max_length=1000,
    style={'base_template': 'textarea.html'}
)

Available Base Templates

The HTMLFormRenderer defines default templates for each field type:
rest_framework/renderers.py
default_style = ClassLookupDict({
    serializers.Field: {
        'base_template': 'input.html',
        'input_type': 'text'
    },
    serializers.EmailField: {
        'base_template': 'input.html',
        'input_type': 'email'
    },
    serializers.URLField: {
        'base_template': 'input.html',
        'input_type': 'url'
    },
    serializers.IntegerField: {
        'base_template': 'input.html',
        'input_type': 'number'
    },
    serializers.BooleanField: {
        'base_template': 'checkbox.html'
    },
    serializers.ChoiceField: {
        'base_template': 'select.html',
    },
    serializers.MultipleChoiceField: {
        'base_template': 'select_multiple.html',
    },
})

Base Template Options

Base TemplateValid Field TypesAdditional Style Options
input.htmlString, numeric, date/time fieldsinput_type, placeholder, hide_label, autofocus
textarea.htmlCharFieldrows, placeholder, hide_label
select.htmlChoiceField or relational fieldshide_label
radio.htmlChoiceField or relational fieldsinline, hide_label
select_multiple.htmlMultipleChoiceField or relational fields with many=Truehide_label
checkbox_multiple.htmlMultipleChoiceField or relational fields with many=Trueinline, hide_label
checkbox.htmlBooleanFieldhide_label
fieldset.htmlNested serializerhide_label
list_fieldset.htmlListField or nested serializer with many=Truehide_label

Customization Examples

Textarea Field:
comment = serializers.CharField(
    max_length=1000,
    style={'base_template': 'textarea.html', 'rows': 10}
)
Hidden Field:
user_id = serializers.IntegerField(
    style={'base_template': 'input.html', 'input_type': 'hidden'}
)
Custom Placeholder:
search = serializers.CharField(
    style={
        'base_template': 'input.html',
        'placeholder': 'Search users...',
        'autofocus': True
    }
)
Radio Buttons (Inline):
gender = serializers.ChoiceField(
    choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other')],
    style={'base_template': 'radio.html', 'inline': True}
)

Custom Templates

Use a completely custom template:
details = serializers.CharField(
    max_length=1000,
    style={'template': 'my-field-templates/custom-input.html'}
)
Custom Template (custom-input.html):
<div class="form-group">
  <label for="{{ field.name }}">{{ field.label }}</label>
  <div class="custom-input-wrapper">
    <input type="text" 
           name="{{ field.name }}" 
           value="{{ field.value|default:'' }}"
           class="form-control custom-input"
           {%% if field.style.placeholder %%}placeholder="{{ field.style.placeholder }}"{%% endif %%}>
  </div>
  {%% if field.errors %%}
    <div class="text-danger">{{ field.errors }}</div>
  {%% endif %%}
</div>

Advanced Usage

Handling Form Validation Errors

class ProfileUpdate(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    template_name = 'profile_form.html'

    def post(self, request, pk):
        profile = get_object_or_404(Profile, pk=pk)
        serializer = ProfileSerializer(profile, data=request.data)
        
        if not serializer.is_valid():
            # Re-render form with errors
            return Response(
                {'serializer': serializer, 'profile': profile},
                status=400
            )
        
        serializer.save()
        return redirect('profile-detail', pk=pk)
Template with Error Display:
{%% load rest_framework %%}

<form method="post">
  {%% csrf_token %%}
  
  {%% if serializer.errors %%}
    <div class="alert alert-danger">
      <strong>Please correct the errors below:</strong>
      <ul>
        {%% for field, errors in serializer.errors.items %%}
          <li>{{ field }}: {{ errors|join:", " }}</li>
        {%% endfor %%}
      </ul>
    </div>
  {%% endif %%}
  
  {%% render_form serializer %%}
  <button type="submit" class="btn btn-primary">Save</button>
</form>

Multiple Forms on One Page

class ProfileAndSettings(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    template_name = 'profile_and_settings.html'

    def get(self, request, pk):
        profile = get_object_or_404(Profile, pk=pk)
        profile_serializer = ProfileSerializer(profile)
        settings_serializer = SettingsSerializer(profile.settings)
        
        return Response({
            'profile_serializer': profile_serializer,
            'settings_serializer': settings_serializer,
            'profile': profile
        })
Template:
{%% load rest_framework %%}

<h2>Profile Information</h2>
<form action="{%% url 'update-profile' pk=profile.pk %%}" method="post">
  {%% csrf_token %%}
  {%% render_form profile_serializer %%}
  <button type="submit">Save Profile</button>
</form>

<h2>Settings</h2>
<form action="{%% url 'update-settings' pk=profile.pk %%}" method="post">
  {%% csrf_token %%}
  {%% render_form settings_serializer %%}
  <button type="submit">Save Settings</button>
</form>

File Upload Forms

class DocumentUpload(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    template_name = 'upload.html'
    parser_classes = [MultiPartParser]

    def post(self, request):
        serializer = DocumentSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return redirect('document-list')
        return Response({'serializer': serializer})
Serializer:
class DocumentSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=200)
    file = serializers.FileField()
    description = serializers.CharField(
        required=False,
        style={'base_template': 'textarea.html'}
    )
Template:
{%% load rest_framework %%}

<form method="post" enctype="multipart/form-data">
  {%% csrf_token %%}
  {%% render_form serializer %%}
  <button type="submit" class="btn btn-primary">Upload</button>
</form>

Template Tags Reference

The rest_framework template tag library provides several useful tags:

render_form

Render a serializer as a form:
{%% load rest_framework %%}
{%% render_form serializer template_pack='rest_framework/horizontal' %%}

render_field

Render a single field:
{%% load rest_framework %%}
{%% render_field serializer.email %%}

add_query_param

Add a query parameter to the current URL:
{%% load rest_framework %%}
<a href="{%% add_query_param request 'page' 2 %%}">Next Page</a>
See the template tags documentation for the complete reference.

Build docs developers (and LLMs) love