叮铛3.0教程

16 \| Building REST APIs

16 | 构建 REST API

Today’s internet is much more than HTML-powered websites. Developers need to support AJAX and native mobile apps. Having tools that support easy creation of JSON, YAML, XML, and other formats is important. By design, a Representational State Transfer (REST) Application Programming Interface (API) exposes application data to other concerns.

今天的互联网不再仅仅是由HTML构建的网站世界. 开发者需要支持AJAX应用以及原生手机应用. 一个可以很容易构建JSON, YAML, XML以及其他数据格式的工具变得尤为重要. 具象状态传输(REST)应用程序编程接口(API)就是这样的工具, 它的设计目的就是为了曝露应用数据给其关注者.

We’ll go over the other side of REST APIs in chapter 17, Consuming REST APIs.

我们将在第17章涵盖REST API的其他方面, 如何使用REST API

{% hint style=‘tip’ %}
PACKAGE TIP: Packages for Crafting APIs

  • django-rest-framework builds off of Django CBVs, adding a wonderful browsable API feature. It has a lot of features, follows elegant patterns, and is great to work with. is library has the de nitive lead in functionality over other similar tools, providing powerful tools for building CBV and FBV powered REST APIs. As of the release of this book, it is the defacto Django tool for building APIs.

  • django-tastypie is a mature API framework that implements its own class-based view system. It’s a feature-rich, mature, powerful, stable tool for creating APIs from Django models. It was created by Daniel Lindsley, the developer who started the django- haystack project, the most commonly used Django search library.

  • For super-quick, super-simple one-off REST API views django-braces (CBVs) and django-jsonview (FBVs) can prove really useful. e downside is that when you get into the full range of HTTP methods and more complex designs, they rapidly become a hindrance due to their lack of focus on building APIs.
    {% endhint %}

{% hint style=‘tip’ %}
包推荐: 制作API的包

  • django-rest-framework builds off of Django CBVs, adding a wonderful browsable API feature. It has a lot of features, follows elegant patterns, and is great to work with. is library has the de nitive lead in functionality over other similar tools, providing powerful tools for building CBV and FBV powered REST APIs. As of the release of this book, it is the defacto Django tool for building APIs.

  • django-tastypie is a mature API framework that implements its own class-based view system. It’s a feature-rich, mature, powerful, stable tool for creating APIs from Django models. It was created by Daniel Lindsley, the developer who started the django- haystack project, the most commonly used Django search library.

  • For super-quick, super-simple one-off REST API views django-braces (CBVs) and django-jsonview (FBVs) can prove really useful. e downside is that when you get into the full range of HTTP methods and more complex designs, they rapidly become a hindrance due to their lack of focus on building APIs.
    {% endhint %}

16.1 Fundamentals of Basic REST API Design

16.1 REST API 设计基本理念

The Hypertext Transfer Protocol(Http) is a protocol for distributing content that provides a set of methods to declare actions. By convention, REST APIs rely on these methods, so use the appropriate HTTP method for each type of action:

Purpose of Request HTTP Method Rough SQL equivalent
Create a new resource POST INSERT
Read an existing resource GET SELECT
Request the metadata of an existing resource HEAD
Update an existing resource PUT UPDATE
Update part of an existing resource PATCH UPDATE
Delete an existing recource DELETE DELETE
Return the supported HTTP methods for the given URL OPTIONS
Echo back the request TRACE
Tunneling over TCP/IP(usually not implemented) CONNECT

Table 16.1: HTTP Methods

超文本传输协议(HTTP)是一种用于分发内容的网络传输协议, 它提供了一组方法来声明动作. 按照约定, REST API依赖这些方法,因此需要对每种类型的动作使用适当的HTTP方法:

请求目的 HTTP 方法 等价SQL语句
创建新资源 POST INSERT
读取指定资源 GET SELECT
请求指定资源的元数据 HEAD
更新指定资源 PUT UPDATE
更新指定资源的一部分 PATCH UPDATE
删除指定资源 DELETE DELETE
返回指定URL所支持的HTTP方法 OPTIONS
回显服务器收到的请求 TRACE
Tunneling over TCP/IP(usually not implemented) CONNECT

表格 16.1: HTTP 方法

A couple of notes on the above:

  • If you’re implementing a read-only API, you might only need to implement GET methods.
  • If you’re implementing a read-write API you must at least also use POST, but should consider using PUT and DELETE.
  • For simplicity, REST API architects sometimes prefer the use of just GET and POST.
  • By definition, GET, PUT, and DELETE are idempotent. POST and PATCH are not.
  • PATCH is often not implemented, but it’s a good idea to implement it if your API supports PUT requests.
  • django-rest-framework and django-tastypie handle all of this for you.

关于上面的几个注释:

  • 如果你要实现一个只读的API, 你可能只需要实现GET方法.
  • 如果你要实现一个读写的API, 你至少需要同时实现POST方法, 但也可以考虑使用PUT或DELETE方法.
  • 为了简单起见,REST API架构师有时更喜欢仅使用GET和POST方法.
  • 根据定义,GET,PUT和DELETE方法是幂等的, POST和PATCH方法却不是.
  • PATCH通常不会去实现,但如果您的API支持PUT请求,则最好实现它.
  • django-rest-framework 和 django-tastypie 将会为你处理所有这些.

Here are some common HTTP status codes that should be considered when implementing a REST API. Note that this is a partial list; a much longer list of status codes can be found at http://en.wikipedia.org/wiki/List_of_HTTP_status_codes.

HTTP Status Code Success/Failure Meaning
200 OK Success GET - Return resouce
PUT - Provide status message or return resource
201 Created Success POST - Provide status message or return newly created resource
204 No Content Success DELETE - Response to successful delete request
304 Unchanged Redirect ALL - Indicates no changes since the last request. Used for checking Last-Modified and Etag headers to improve performance.
400 Bad Request Failure ALL - Return error messages, including form validation error.
401 Unauthorized Failure ALL - Authentication required but use did not provide credentials.
403 Forbidden Failure ALL - User attempted to access restricted content
404 Not Found Failure ALL - Resource is not found
405 Method Not Allowed Failure ALL - An unallowed HTTP method was attempted.
410 Gone Failure ALL - A method was attempted that is no longer supported. Used when an API is shut down in favor of a newer version of an API. Mobile applications can test for this condition, and if it occurs, tell the user to upgrade.
429 Too Many Requests Failure ALL - The user has sent too many requests in a given amount of time. Intended for use with rate limiting.

Table 16.2: HTTP Status Codes

以下是在实现REST API时应考虑的一些常见HTTP状态代码. 需要注意的是这只是一部分, 更详细的状态代码列表可以从以下这个链接 http://en.wikipedia.org/wiki/List_of_HTTP_status_codes 中找到.

HTTP状态码 成功/失败 意义
200 OK 成功 GET - 返回资源
PUT - 提供状态消息或返回资源
201 Created 成功 POST - 提供状态消息或返回新创建的资源
204 No Content 成功 DELETE - 对成功的删除请求的响应
304 Unchanged 跳转 ALL - 表示自上次请求以来没有发生更改. Used for checking Last-Modified and Etag headers to improve performance.
400 Bad Request 失败 ALL - 返回错误消息, 包括表单验证错误.
401 Unauthorized 失败 ALL - 需要验证, 但使用时未提供凭据.
403 Forbidden 失败 ALL - 用户尝试访问受限制的内容
404 Not Found 失败 ALL - 找不到资源
405 Method Not Allowed 失败 ALL - 尝试了不允许的HTTP方法.
410 Gone 失败 ALL - 尝试了不再支持的方法. 当希望关闭旧版本API以支持新版本API时使用. 移动端应用程序可以测试这种情况,当它发生时告诉用户去升级.
429 Too Many Requests 失败 ALL - 用户在给定时间内发送了太多请求. 适用于请求频率的限制.

表格 16.2: HTTP 状态码

16.2 Implementing a Simple JSON API

16.2 实现简单的JSON API

Let’s use the flavors app example from previous chapters as our base, providing the capability to create, read, update, and delete flavors via HTTP requests using AJAX, python-requests, or some other library. We’ll also use django-rest-framework, as it provides us with the capability to build a REST API quickly using patterns similar to the class-based views that we describe in previous chapters. We’ll begin by listing the Flavor model again:


Example 16.1

# flavors/models.py
from django.core.urlresolvers import revers
from django.db import models

class Flavor(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
scoops_remaining = models.IntegerField(default=0)
def get_absolute_url(self):
return reverse("flavors:detail", kwargw={"slug": self.slug})

Define the serializer class:
定义序列化类:

Example 16.2

from rest_framework import serializers

from .models import Flavor

class FlavorSerializer(serializers.ModelSerializer):
class Meta:
model = Flavor
fields = ('title', 'slug', 'scoops_remaining')

Now let’s add in some views:
现在让我们添加一些视图:

Example 16.3

# flavors/views
from rest_framework.generics import ListCreateAPIView
from rest_framework.generics import RetrieveUpdateDestroyAPIView
from .models import Flavor
from .serializers import FlavorSerializer

class FlavorCreateReadView(ListCreateAPIView):
queryset = Flavor.objects.all()
serializer_class = FlavorSerializer
lookup_field = 'slug'

class FlavorReadUpdateDeleteView(RetrieveUpdateDestroyAPIView):
queryset = Flavor.objects.all()
serializer_class = FlavorSerializer
lookup_field = 'slug'

We’re done! Wow, that was fast!

{% hint style=‘tip’ %}
TIP: Classy Django REST Framework is a Useful Reference

For working with the Django Rest Framework, we’ve found that http://cdrf.co is a great
cheat sheet. It is patterned after the famous http://ccbv.co.uk reference site, but tailored
for Django Rest Framework.
{% endhint %}

{% hint style=‘tip’ %}
提示:Classy Django REST Framework是一个有用的参考

For working with the Django Rest Framework, we’ve found that http://cdrf.co is a great
cheat sheet. It is patterned after the famous http://ccbv.co.uk reference site, but tailored
for Django Rest Framework.
{% endhint %}

Now we’ll wire this into our flavors/urls.py module:


Example 16.4

# flavors/urls.py
from django.conf.urls import url

from flavors import views

urlpatterns = [
url(
regex=r"^api/$",
view=views.FlavorCreateReadView.as_view(),
name="flavor_rest_api"
),
url(
regex=r"^api/(?P<slug>[-\w]+)/$",
view=views.FlavorReadUpdateDeleteView.as_view(),
name="flavor_rest_api"
)

What we are doing is resuing the same view and URLConf name, making it easier to manage when you have a need for a JavaScript-heavy front-end. All you need to do is access the Flavor reource via the {% url %} template tag.

In case it’s not clear exactly what our URLConf is doing, let’s review it with a table:

Url View Url Name(same)
/flavors/api/ FlavorCreateReadView flavor_rest_api
/flavors/api/:slug/ FlavorReadUpdateDeleteView flavor_rest_api

Table 16.3: URLConf for the Flavor REST APIs

{% hint style=‘danger’ %}
WARNING: Our Simple API Does Not Use Permissions

If you implement an API using our example, don’t forget to authenticate users and assign them permissions appropriately!

The end result is the traditional REST-style API definition:

Example 16.5
flavors/api/
flavors/api/:slug/

{% hint style=‘tip’ %}
TIPS: Common Syntax for Describing REST APIs

It’s not uncommon to see syntax like what is described in Example 14.4. In this particular case, /flavors/api/:slug/ includes a :slug value. is represents a variable, but in a manner suited for documentation across frameworks and languages, and you’ll see it used in many third-party REST API descriptions.

{% endhint %}

We’ve shown you (if you didn’t know already) how it’s very easy to build REST APIs in Django, now let’s go over some advice on maintaining and extending them.

16.3 REST API Architecture

Building quick APIs is easy with tools like django-rest-framework and django-tastypie, but extending and maintaining them to match your project’s needs takes a bit more thought.

16.3.1 Code for a Project Should Be Neatly Organized

For projects with a lot of small, interconnecting apps, it can ben hard to hunt down where a particular API view lives. In contrast to placing all API code within the each relevant app, sometimes it makes more sense to build an app specifically for the API. This is where all the serializers, renderers, and views are placed. Of course, the name of the app should reflect its API version (see subsection 16.3.6).

For example, we might place all our views, serializers, and other API components in an app titled apiv4.

The downside is the possibility for the API app to become too large and disconnected from the apps that power it. Hence why we consider an alternative in the next subsection.

16.3.2 Code for an App Should Remain in the App

When it comes down to it, REST APIs are just veiws, For simpler, smaller projects, REST API views should go into views.py or viewsets.py modules and follow the same guidelines we endorse when it comes to any other view. The same goes for app- or model-specific serializers and renderers. If we do have app-specific serializers or renderers, the same applies.

For larger projects with too many REST API view classes for a single views.py/viewsets.py module, we can break them up. In other words, move the view classes into a viewset package complete with init.py and Python modules containing a smaller number of REST API views.

The downside is that if there are too many small, interconnecting apps, it can be hard to keep track of the myriad of places API components are placed. Hence why we considered another approach in the previous subsection.

16.3.3 Try to Keep Business Logic Out of API Views

Regardless of which architectural approach you take, it’s a good idea to try to keep as much logic as prossible out of API views. If this sounds familiar, it should. We covered this in ‘Try to Keep Business Logic out of Views’, chapter 8 Function- and Class-Based Views, and remember, API views are just another type of view, after all.

16.3.4 Grouping API URLs

If you have REST API views in multiple Django apps, how do you build a project-wide API that looks like this?


Example 16.6
api/flavors/ # GET, POST
api/flavors/:slug/ # GET, PUT, DELETE
api/users/ # GET, POST
api/users/:slug/ # GET, PUT, DELETE

In the past, we placed all API view code into a dedicated Django app called api or apiv1, with custom logic in some of the REST views, serializers, and more. In theory it’s a pretty good approach, but in practice it means we have logic for a particular app in more than just on location.

Our current approach is to lean on URL configuration. When building a project-wide API we wirte the REST views in the views.py or viewsets.py modules, wire them into a URLConf called something like core/api.py or core/apiv1.py and include that from the project root’s urls.py module. This means that we might have something like the following code:


Example 16.7

# core/api.py
"""Called from the project root's urls.py URLConf thus:
url(r"^api/", include("core.api", namespace="api")),
"""
from django.conf.urls import url

from flavors import views as flavor_views
from users import views as user_views

urlpatterns = [
# {% url "api:flavors" %}
url(
regex=r"ˆflavors/$",
view=flavor_views.FlavorCreateReadView.as_view(),
name="flavors"
),
# {% url "api:flavors" flavor.slug %}
url(
regex=r"ˆflavors/(?P<slug>[-\w]+)/$",
view=flavor_views.FlavorReadUpdateDeleteView.as_view(),
name="flavors"
),
# {% url "api:users" %}
url(
regex=r"ˆusers/$",
view=user_views.UserCreateReadView.as_view(),
name="users"
),
# {% url "api:users" user.slug %}
url(
regex=r"^users/(?P<slug>[-\w]+)/$",
view=user_views.UserReadUpdateDeleteView.as_view(),
name="users"
),
]

16.3.5 Test Your API

We find that Django’s test suite makes it really easy to test API implementations. It’s certainly much easier than staring at curl results! Testing is covered at length in chapter 22, Testing Stinks and Is a Waste of Money!, and we even include in taht chapter the tests we wrote for our simple JSON API(see subsection 22.3.1).

16.3.6 Version Your API

It’s a good practice to abbreviate the urls of your API with the version number e.g. /api/v1/flavors or /api/v1/users and then as the API changes, /api/v2/flavors or /api/v2/users. When the version number changes, existing customers can continue to use the previous version without unknowingly breaking their calls to the API.

Also, in order to avoid angering API consumers, it’s critical to maintain both the existing API and the predecessor API during and after upgrades. It’s not uncommon for the deprecated API to remain in use for several months.

When you do implement an API, provide customers/users with a deprecation warning along with ample time so they can perform necessary upgrades and not break their own applications. From personal experience, the ability to send a deprecation warning to end users is an excellent reason to request email addresses from users of even free and open source API services.

16.4 Service-Oriented Architecture

In Service-Oriented Architecture, or SOA, a web application is broken up into isolated, independent components. Each component might run on its own server or cluster, with the components com-municating with one another. For SOA-style Django projects, communication between components typically occurs via REST APIs.

The main reason to follow SOA is to make it easier for many engineers to work on different compo- nents of a web application without running into con icts with one another. Instead of having a giant team of 100 engineers all working on the same codebase simultaneously, there might be 10 teams of 10 engineers, each team working on an isolated SOA component.
For smaller team projects, we’ve found that SOA tends to cause more trouble than it’s worth. Having more moving parts causes greater complexity.
To implement an SOA-style web application, think about how to break your Django project into isolated web applications, each running independently. Each Django app might get split off into its own isolated Django project.
For example, if you’re implementing the Airbnb of Ice Cream Truck Rentals, you might start with a single Django project containing 8 loosely-coupled apps: trucks, owners, renters, payments, receipts, reservations, scheduling, and reviews. As your company grows, you might refactor each app into its own REST API-powered Django project, maintained by its own team.

16.5 Shutting Down an External API

When it’s time to shut down an older version of an external API in favor of a new one, here are useful steps to follow:

16.5.1 Step #1: Notify Users of Pending Shut Down

Provide as much advanced notice as possible. Preferably six months, but as short as one month. Inform API users via email, blogs, and social media. We like to report the shutdown noti cation to the point that we worry people are getting tired of the message.

16.5.2 Step #2: Replace API With 410 Error View

When the API is nally shut down, we provide a simple 410 Error View. We include a very simple message that includes the following information:

  • A link to the new API’s endpoint.
  • A link to the new API’s documentation.
  • A link to an article describing the details of the shut down.

Below is a sample shutdown view that works against any HTTP method:

Example 16.8


# core/apiv1_shutdown.py
from django.http import HttpResponseGone

apiv1_gone_msg = """APIv1 was removed on April 2, 2015. Please switch to APIv3:
<ul>
<li>

<a href="https://www.example.com/api/v3/">APIv3 Endpoint</a>
</li>
<li>

<a href="https://example.com/apiv3_docs/">APIv3 Documentation</a>

</li>
<li>
<a href="http://example.com/apiv1_shutdown/">APIv1 shut down notice</a>
</li>

</ul>
"""

def apiv1_gone(request):
return HttpResponseGone(apiv1_gone_msg)

16.6 Evaluating REST Frameworks

When you begin considering REST frameworks, it’s worthwhile to consider the following:

16.6.1 Django Rest Framework Is the Defacto Package

The vast majority of Django projects being built at the time of the release of this book use Django Rest Framework. is popularity ensures that the package will be maintained in the future, and that nding other Django coders to work on the project will be easier than if we used an alternative.

16.6.2 How Much Boilerplate Do You Want to Write?

These days, most frameworks make an effort to reduce boilerplate when dealing with resources. In fact, usually when we hear grumbling on the lines of ‘X framework has too much boilerplate!!!’ it is actually a matter of trying to gure out how to implement Remote Procedure Calls in a resource- driven system.

Speaking of Remote Procedure calls, let’s move on to the next sub-section…

16.6.3 Are Remote Procedure Calls Easy to Implement?

The resource model used by REST frameworks to expose data is very powerful, but it doesn’t cover ev- ery case. Speci cally, resources don’t always match the reality of application design. For example, it is easy to represent syrup and a sundae as two resources, but what about the action of pouring syrup? Us- ing this analogy, we change the state of the sundae and decrease the syrup inventory by one. While we could have the API user change things individually, that can generate issues with database integrity. erefore in some cases it can be good idea to present a method like sundae.pour syrup(syrup) to the client as part of the RESTful API.

In computer science terms, sundae.pour syrup(syrup) could be classi ed as a Remote Proce- dure Call or RPC.

Depending on the REST framework or system you chose, RPC calls can be easy or challenging to implement. is is an area that needs to be investigated early on, as it’s painful to discover in the middle of a project that a framework of choice makes this a pain point.

Additional Reading:
* https://en.wikipedia.org/wiki/Remote_Procedure_Call
* https://en.wikipedia.org/wiki/Resource-oriented_architecture

16.6.4 CBVs or FBVs?

As mentioned in the previous chapters, the authors prefer CBVs, but other developers prefer FBVs. If this is a sticking point, then consider exploring options that support FBV-based implementations such as django-rest-framework.

16.7 Rate Limiting Your API

Rate limiting is when an API restricts how many requests can be made by a user of the API within a period of time. is is done for a number of reasons, which we’ll explain below.

16.7.1 Unfettered API Access is Dangerous

In the ancient times (2010) we launched the Django Packages website. e project, started during the Django Dash contest, was an instant hit for the Django community. We sprinted on it constantly, and its feature set grew rapidly. Unfortunately, we quickly hit the rate limit of GitHub’s rst API. is meant that after a certain amount of API requests per hour, we weren’t allowed to make any more until a new hour passed.

Fortunately at DjangoCon 2010 we had the opportunity to ask one of the founders of GitHub if we could have unlimited access to their API. He graciously said ‘yes’ and within a day we could get data from GitHub as much as we wanted.

We were delighted. Our users were delighted. Usage of the site increased, people were thirsty for data as to what were the most active projects. So desirous of data were we that every hour we requested the latest data from GitHub. And that caused a problem for GitHub.

You see, this was 2010 and GitHub was not the giant, powerful company it is today. At 17 minutes past each hour, Django Packages would send thousands of requests to the GitHub API in a very short period. With unfettered access to GitHub application of the time, we were causing them problems.

Eventually, GitHub contacted us and requested that we scale back how much we were using their API. We would still have unlimited access, just needed to give them breathing room. We complied, checking data once per day instead of by the hour, and at a more reasonable rate. We continue to do so to this day.

While modern GitHub can certainly handle much, much larger volumes of API access then it could in late 2010, we like to think we learned a shared lesson about unfettered access to an API: Grant such access cautiously.

16.7.2 REST Frameworks Must Come with Rate Limiting

One of the features usually delegated to a feature bullet when describing a REST API framework is if it allows for rate limiting of API users. Yet when evaluating which one to use, this is a must-have. Being able to control this can mean the difference between joyful triumph or utter disaster.

{% hint style=‘tip’ %}
TIP: HTTP Server Rate Limiting

It’s possible to use nginx or apache for rate limiting. e upside is faster performance. e downside is that it removes this functionality from the Python code.
{% endhint %}

16.7.3 Rate Limit Can Be A Business Plan

Imagine we launch an API-based startup that lets users add images of toppings to images of ice cream. We know that everyone will want to use this API, and come up with several tiers of access that we tie into pricing:

Developer tier is free, but only allows 10 API requests per hour.
One Scoop is \(24/month, allows 25 requests per minute.
**Two Scoops** is \)79/month, allows 50 requests per minute.
Corporate is $5000/month, allows for 200 requests per minute.

Now all we have to do is get people to use our API.

6.8 Advertising Your REST API

Let’s assume we’ve built our REST API and want outside coders and companies to use it. How do we go about doing that?

16.8.1 Documentation

The most important thing to do is to provide comprehensive documentation. e easier to read and understand the better. Providing easy-to-use code examples is a must. You can write it from scratch, use the auto-documentation provided by tools like django-rest-framework, Sphinx, and Markdown or even embrace a commercial documentation generation service like swagger.io.
Some of the material in chapter 23, ‘Documentation: Be Obsessed’ might prove useful for forward- facing REST API documentation.

16.8.2 Provide Client SDKs

Something that may help spread use of your API is to provide a software development kits (SDK) for various programming languages. e more programming languages covered the better. For us, we’ve found the must-have languages include Python, JavaScript, Ruby, PHP, Go, and Java.

In our experience, it’s a good idea to write at least one of these libraries ourselves and create a demo project. e reason is that it not only advertises our API, it forces us to experience our API from the same vantage point as our consumers.

For building client SDKs, reading section 21.9, ‘Releasing Your Own Django Packages’ might prove useful.

16.9 Additional Reading

We highly recommend reading the following:
* http://en.wikipedia.org/wiki/REST
* http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
* http://jacobian.org/writing/rest-worst-practices/

16.10 Summary

In this chapter we covered:
* API creation libraries.
* Grouping strategies.
* Fundamentals of basic REST API design.
* Implementing a simple JSON API.

Coming up next, we’ll go over the other side of REST APIs in chapter 17, Consuming REST APIs.