django-rest-framework/tutorial/5-relationships-and-hyperlinked-apis.html
2014-11-03 11:21:29 +00:00

373 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Tutorial 5: Relationships & Hyperlinked APIs - Django REST framework</title>
<link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="canonical" href="http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django, API, REST, Tutorial 5: Relationships & Hyperlinked APIs">
<meta name="author" content="Tom Christie">
<!-- Le styles -->
<link href="http://www.django-rest-framework.org/css/prettify.css" rel="stylesheet">
<link href="http://www.django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
<link href="http://www.django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
<link href="http://www.django-rest-framework.org/css/default.css" rel="stylesheet">
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-18852272-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
<style>
span.fusion-wrap a {
display: block;
margin-top: 10px;
color: black;
}
a.fusion-poweredby {
display: block;
margin-top: 10px;
}
@media (max-width: 767px) {
div.promo {display: none;}
}
</style>
</head>
<body onload="prettyPrint()" class="5-relationships-and-hyperlinked-apis-page">
<div class="wrapper">
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
<a class="repo-link btn btn-inverse btn-small " href="../tutorial/6-viewsets-and-routers">Next <i class="icon-arrow-right icon-white"></i></a>
<a class="repo-link btn btn-inverse btn-small " href="../tutorial/4-authentication-and-permissions"><i class="icon-arrow-left icon-white"></i> Previous</a>
<a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
<div class="nav-collapse collapse">
<ul class="nav">
<li><a href="http://www.django-rest-framework.org">Home</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="http://www.django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
<li><a href="http://www.django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
<li><a href="http://www.django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
<li><a href="http://www.django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
<li><a href="http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
<li><a href="http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
<li><a href="http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="http://www.django-rest-framework.org/api-guide/requests">Requests</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/responses">Responses</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/views">Views</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/routers">Routers</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/parsers">Parsers</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/renderers">Renderers</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/serializers">Serializers</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/authentication">Authentication</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/permissions">Permissions</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/throttling">Throttling</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/filtering">Filtering</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/pagination">Pagination</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/testing">Testing</a></li>
<li><a href="http://www.django-rest-framework.org/api-guide/settings">Settings</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="http://www.django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
<li><a href="http://www.django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
<li><a href="http://www.django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
<li><a href="http://www.django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
<li><a href="http://www.django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
<li><a href="http://www.django-rest-framework.org/topics/third-party-resources">Third Party Resources</a></li>
<li><a href="http://www.django-rest-framework.org/topics/contributing">Contributing to REST framework</a></li>
<li><a href="http://www.django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
<li><a href="http://www.django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
<li><a href="http://www.django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
<li><a href="http://www.django-rest-framework.org/topics/2.4-announcement">2.4 Announcement</a></li>
<li><a href="http://www.django-rest-framework.org/topics/kickstarter-announcement">Kickstarter Announcement</a></li>
<li><a href="http://www.django-rest-framework.org/topics/release-notes">Release Notes</a></li>
<li><a href="http://www.django-rest-framework.org/topics/credits">Credits</a></li>
</ul>
</li>
</ul>
<ul class="nav pull-right">
<!-- TODO
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Version: 2.0.0 <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#">Trunk</a></li>
<li><a href="#">2.0.0</a></li>
</ul>
</li>
-->
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="body-content">
<div class="container-fluid">
<!-- Search Modal -->
<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 id="myModalLabel">Documentation search</h3>
</div>
<div class="modal-body">
<!-- Custom google search -->
<script>
(function() {
var cx = '015016005043623903336:rxraeohqk6w';
var gcse = document.createElement('script');
gcse.type = 'text/javascript';
gcse.async = true;
gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
'//www.google.com/cse/cse.js?cx=' + cx;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(gcse, s);
})();
</script>
<gcse:search></gcse:search>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</div>
</div>
<div class="row-fluid">
<div class="span3">
<!-- TODO
<p style="margin-top: -12px">
<a class="btn btn-mini btn-primary" style="width: 60px">&laquo; previous</a>
<a class="btn btn-mini btn-primary" style="float: right; margin-right: 8px; width: 60px;">next &raquo;</a>
</p>
-->
<div id="table-of-contents">
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
<li class="main"><a href="#tutorial-5-relationships-&-hyperlinked-apis">Tutorial 5: Relationships & Hyperlinked APIs</a></li>
<li><a href="#creating-an-endpoint-for-the-root-of-our-api">Creating an endpoint for the root of our API</a></li>
<li><a href="#creating-an-endpoint-for-the-highlighted-snippets">Creating an endpoint for the highlighted snippets</a></li>
<li><a href="#hyperlinking-our-api">Hyperlinking our API</a></li>
<li><a href="#making-sure-our-url-patterns-are-named">Making sure our URL patterns are named</a></li>
<li><a href="#adding-pagination">Adding pagination</a></li>
<li><a href="#browsing-the-api">Browsing the API</a></li>
<div class="promo">
</div>
</ul>
</div>
</div>
<div id="main-content" class="span9">
<h1 id="tutorial-5-relationships-hyperlinked-apis">Tutorial 5: Relationships &amp; Hyperlinked APIs</h1>
<p>At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.</p>
<h2 id="creating-an-endpoint-for-the-root-of-our-api">Creating an endpoint for the root of our API</h2>
<p>Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the <code>@api_view</code> decorator we introduced earlier. In your <code>snippets/views.py</code> add:</p>
<pre class="prettyprint lang-py"><code>from rest_framework import renderers
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
@api_view(('GET',))
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})
</code></pre>
<p>Notice that we're using REST framework's <code>reverse</code> function in order to return fully-qualified URLs.</p>
<h2 id="creating-an-endpoint-for-the-highlighted-snippets">Creating an endpoint for the highlighted snippets</h2>
<p>The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints.</p>
<p>Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two styles of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.</p>
<p>The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.</p>
<p>Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own <code>.get()</code> method. In your <code>snippets/views.py</code> add:</p>
<pre class="prettyprint lang-py"><code>from rest_framework import renderers
from rest_framework.response import Response
class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = (renderers.StaticHTMLRenderer,)
def get(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
</code></pre>
<p>As usual we need to add the new views that we've created in to our URLconf.
We'll add a url pattern for our new API root in <code>snippets/urls.py</code>:</p>
<pre class="prettyprint lang-py"><code>url(r'^$', 'api_root'),
</code></pre>
<p>And then add a url pattern for the snippet highlights:</p>
<pre class="prettyprint lang-py"><code>url(r'^snippets/(?P&lt;pk&gt;[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),
</code></pre>
<h2 id="hyperlinking-our-api">Hyperlinking our API</h2>
<p>Dealing with relationships between entities is one of the more challenging aspects of Web API design. There are a number of different ways that we might choose to represent a relationship:</p>
<ul>
<li>Using primary keys.</li>
<li>Using hyperlinking between entities.</li>
<li>Using a unique identifying slug field on the related entity.</li>
<li>Using the default string representation of the related entity.</li>
<li>Nesting the related entity inside the parent representation.</li>
<li>Some other custom representation.</li>
</ul>
<p>REST framework supports all of these styles, and can apply them across forward or reverse relationships, or apply them across custom managers such as generic foreign keys.</p>
<p>In this case we'd like to use a hyperlinked style between entities. In order to do so, we'll modify our serializers to extend <code>HyperlinkedModelSerializer</code> instead of the existing <code>ModelSerializer</code>.</p>
<p>The <code>HyperlinkedModelSerializer</code> has the following differences from <code>ModelSerializer</code>:</p>
<ul>
<li>It does not include the <code>pk</code> field by default.</li>
<li>It includes a <code>url</code> field, using <code>HyperlinkedIdentityField</code>.</li>
<li>Relationships use <code>HyperlinkedRelatedField</code>,
instead of <code>PrimaryKeyRelatedField</code>.</li>
</ul>
<p>We can easily re-write our existing serializers to use hyperlinking. In your <code>snippets/serializers.py</code> add:</p>
<pre class="prettyprint lang-py"><code>class SnippetSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.Field(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
model = Snippet
fields = ('url', 'highlight', 'owner',
'title', 'code', 'linenos', 'language', 'style')
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail')
class Meta:
model = User
fields = ('url', 'username', 'snippets')
</code></pre>
<p>Notice that we've also added a new <code>'highlight'</code> field. This field is of the same type as the <code>url</code> field, except that it points to the <code>'snippet-highlight'</code> url pattern, instead of the <code>'snippet-detail'</code> url pattern.</p>
<p>Because we've included format suffixed URLs such as <code>'.json'</code>, we also need to indicate on the <code>highlight</code> field that any format suffixed hyperlinks it returns should use the <code>'.html'</code> suffix.</p>
<h2 id="making-sure-our-url-patterns-are-named">Making sure our URL patterns are named</h2>
<p>If we're going to have a hyperlinked API, we need to make sure we name our URL patterns. Let's take a look at which URL patterns we need to name.</p>
<ul>
<li>The root of our API refers to <code>'user-list'</code> and <code>'snippet-list'</code>.</li>
<li>Our snippet serializer includes a field that refers to <code>'snippet-highlight'</code>.</li>
<li>Our user serializer includes a field that refers to <code>'snippet-detail'</code>.</li>
<li>Our snippet and user serializers include <code>'url'</code> fields that by default will refer to <code>'{model_name}-detail'</code>, which in this case will be <code>'snippet-detail'</code> and <code>'user-detail'</code>.</li>
</ul>
<p>After adding all those names into our URLconf, our final <code>snippets/urls.py</code> file should look something like this:</p>
<pre class="prettyprint lang-py"><code># API endpoints
urlpatterns = format_suffix_patterns([
url(r'^$', views.api_root),
url(r'^snippets/$',
views.SnippetList.as_view(),
name='snippet-list'),
url(r'^snippets/(?P&lt;pk&gt;[0-9]+)/$',
views.SnippetDetail.as_view(),
name='snippet-detail'),
url(r'^snippets/(?P&lt;pk&gt;[0-9]+)/highlight/$',
views.SnippetHighlight.as_view(),
name='snippet-highlight'),
url(r'^users/$',
views.UserList.as_view(),
name='user-list'),
url(r'^users/(?P&lt;pk&gt;[0-9]+)/$',
views.UserDetail.as_view(),
name='user-detail')
])
# Login and logout views for the browsable API
urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
]
</code></pre>
<h2 id="adding-pagination">Adding pagination</h2>
<p>The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages.</p>
<p>We can change the default list style to use pagination, by modifying our <code>settings.py</code> file slightly. Add the following setting:</p>
<pre class="prettyprint lang-py"><code>REST_FRAMEWORK = {
'PAGINATE_BY': 10
}
</code></pre>
<p>Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well separated from your other project settings.</p>
<p>We could also customize the pagination style if we needed too, but in this case we'll just stick with the default.</p>
<h2 id="browsing-the-api">Browsing the API</h2>
<p>If we open a browser and navigate to the browsable API, you'll find that you can now work your way around the API simply by following links.</p>
<p>You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the highlighted code HTML representations.</p>
<p>In <a href="6-viewsets-and-routers">part 6</a> of the tutorial we'll look at how we can use ViewSets and Routers to reduce the amount of code we need to build our API.</p>
</div><!--/span-->
</div><!--/row-->
</div><!--/.fluid-container-->
</div><!--/.body content-->
<div id="push"></div>
</div><!--/.wrapper -->
<footer class="span12">
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a></p>
</footer>
<!-- Le javascript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="http://www.django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
<script src="http://www.django-rest-framework.org/js/prettify-1.0.js"></script>
<script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
<script>
//$('.side-nav').scrollspy()
var shiftWindow = function() { scrollBy(0, -50) };
if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow);
$('.dropdown-menu').on('click touchstart', function(event) {
event.stopPropagation();
});
// Dynamically force sidenav to no higher than browser window
$('.side-nav').css('max-height', window.innerHeight - 130);
$(function(){
$(window).resize(function(){
$('.side-nav').css('max-height', window.innerHeight - 130);
});
});
</script>
</body></html>