---
title: Django for APIs
tags: learning notes, Django, Django REST Framework
---
[TOC]
# Introduction
RESTful APIs 對網路產生非常巨大的影響。因為即使是最單純的網路運作都包含多台電腦彼此互相交流。
API (Application Programming Interface) ,使用一個正式的方式來描述兩台電腦如何直接與彼此溝通。有很多方法可以做成 RESTful 模式的 API 、 web APIs (允許在 world wide web 傳遞資料的 API) 。
## 為什麼要使用 API ?
Django 第一版是在 2005 年發布,當時絕大多數的網站都會有一個整體的程式庫。「後端」包含 database models 、 URLs 、 views ,並與「前端」HTML, CSS 的 templates 、 JavaScript 控制網頁表面樣式來互動。
然而,近年來的 "API first" 的方法可以說在網站開發中逐漸成為主導的範式,這個模式正式將後端從前端拆分,這代表 Django 變成很強大的資料庫和 API ,而不僅僅是網路框架。
如今, Django 在大型公司的主流運用更常是後端的 API 而不是完整的網站解決方案。
但為何如此?因為傳統的 Django 本身運作得相當好,但將 Django 網站轉換到 Web API 似乎要做許多工作。另外,作為開發人員,你還需要用另外一種語言寫前端。
這種將服務區分到不同的部件的方法稱為 SOA (Service-oriented architecture) ,這種前後端拆分方法有很多好處,第一,它不會過時,因為任何的 JavaScript 前端都可以使用後端的 API ,有鑒於前端庫的劇烈改變 (React 在 2013 年發佈、 Vue 在 2014 年發佈),所以這具有高價值。當又被更新的一個前端框架所取代時,後端 API 可以保持一樣,不用大幅更動。
第二, API 可以支援多個用不同語言、不同框架寫的前端。試想 JavaScript 絕大多數被用來寫網站前端、 Java 被用來寫 Android 應用程式、 Swift 被用來寫 iOS 應用程式,以傳統的完整方式,一個 Django 網站無法支援這麼多種前端,但有了內部 API ,這三個前端都可以跟同一個後端溝通。
第三,在內部或在外部都可以使用 “API first” 方法,當我 2010 年在 Quizlet 服務時,我們沒有資源去開發我們自己的 Android APP 或是 iOS APP,但我們有外部可用的 API ,有 30 多個開發人員使用 Quizlet 的資料庫,來開發自己的單字卡應用程式。這些應用程式總合被下載次數超過一百萬次,同時滋養了開發人員,也擴大 Quizlet 的影響範圍。
"API first" 這個方法相較於傳統的 Django ,主要的缺點是需要更多的配置。而 Django REST Framework 很大程度地降低這些複雜度。
## Django REST Framework
Django 有為數眾多的[第三方套件](https://djangopackages.org/)來強化功能, Django REST Framework 可以說是殺手級套件,成熟、多功能、可以客制、可以測試,還有完整的文件,刻意臨摹傳統 Django 的慣例以便更好上手,而且是用 Python 寫成的。用少量的程式碼將現存的 Django application 轉換成 Web API。
>> CH1: begins with a brief introduction to web APIs and the HTTP protocol
>> CH2: we review the differences between traditional Django and Django REST Framework by building out a Library book website and then adding an API to it.
>> CH3-CH4: we build a Todo API and connect it to a React front-end. The same process can be used to connect any dedicated front-end—web, iOS, Android, desktop, or other—to a web API back-end.
>> CH5-CH9: we build out a production-ready Blog API which includes full CRUD functionality. We also cover in-depth permissions, user authentication, viewsets, routers, documentation, and more.
# Web APIs
在打造 Web API 之前,要先了解網路的運作原理,因為 Web API 是在 WWW 傳遞資料的 API 啊!本章主要複習 Web API 會用到的專有名詞,包含 endpoint、resource、HTTP verbs、HTTP status code 和 REST 。
## World Wide Web
在 1989 年一名 CERN 的學者 Tim Berners-Lee 發明了 HTTP ,開啟現代網路的新篇章。將所謂的 hypertext (擁有超連結的文件) 搬到網路上。
他發明的 HTTP 將「文件分享到網路上」這件事標準化,????? 時至今日,大部分人想到的「網路」就是 World Wide Web ,因為它是最主要的方式串連所有人和電腦。
## URLs
URL (Uniform Resource Locator) 就是一個 resource 在網路上的地址。
簡化來說,當你今天在瀏覽器輸入 Google 的 URL ,你的瀏覽器就會送出一個請求給 server , server 會回應你請求的資料。這個「請求-回應」的模式就是網路溝通方式的基礎。所以網路溝通就是透過 HTTP 請求和 HTTP 回應。
每個 URL 都包含 **scheme** 和 **hostname**:
- scheme:
- `http` or `https` for website
- `ftp` for files
- `smtp` for email
- and so on...
- hostname
## Internet Protocol Suite
整個網路架構的模型以及使用的架構泛稱為 [Internet Protocol Suite](https://en.wikipedia.org/wiki/Internet_protocol_suite),當你在瀏覽器輸入 `https://www.google.com` ,發生了什麼事?
- 遵循 IP 協議 (Internet Protocol),每個連上網的設備都有一個獨一無二的 IP address 。瀏覽器向 Domain Name Server 請求該 URL 的 IP address 。
- 取得 IP address 之後,要向造訪的 server 建立一個保證傳的到的方法,也就是遵循 TCP 協議 (Transmission Control Protocol), 使用三方交握的方式確認,確認完畢後就可以開始以 HTTP 進行溝通。
## HTTP verbs
這邊僅簡單說明網站中的動作類別恰好與 HTTP 方法對應。我們常在網站中執行的動作類別稱為 **CRUD**,與 HTTP 方法對應如下:
- Create <> POST
- Read <> GET
- Update <> PUT
- Delete <> DELETE
## Endpoints
一個網站包含很多網頁,裡面有 HTML, CSS, Images, JavaScript 等等,而一個網站的 Web API 有很多端點來存取資訊,這些端點是帶有 HTTP verb 的 URL 。
端點類型可以回傳多個資料來源的就是 **collection**。
## HTTP
HTTP 是一個「請求-回應」的協議,奠基在兩台電腦已經存在 TCP 連結之上。發出請求的就是客戶端,回應的就是伺服器端。
每一個 HTTP 訊息包含:
- 1 個 request/status line
- 一些 [headers](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields)
- 還有 optional ,只有「回應」時會有的 body
絕大多數網頁都有多個 resources ,就需要多個「請求-回應」的循環。假設有一個網頁有 HTML 、 一個 CSS 檔案、一張 image ,那就要來回三次才能將完整的網頁提供給瀏覽器。
## Status code
當你的瀏覽器發出請求時,[狀態碼](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)會伴隨著回應回傳回來,主要形式如以下:
- 2xx Success: 客戶端請求的動作已經收到、了解並接受。
`200(OK), 201(Created)`
- 3xx Redirection: 客戶端請求的 URL 已經被移走了。
`301(Moved Permanently)`
- 4xx Client Error: 有錯誤,主要是客戶端發出的錯誤 URL 請求。
`404(Not Found), 403(Forbidden)`
- 5xx Server Error: 伺服器端解析請求失敗。
`500(Server Error)`
## Statelessness
HTTP 是 stateless protocol (無狀態協議),代表每一對的「請求-回應」都是與之前的「請求-回應」各自獨立。
可以想像好處相當的多,處理起來相當彈性,但相對於 Web application 來說,管理狀態就變得非常重要。「狀態」就是一個網站記得你是否登入、一個電商網站管理你的購物車等等。
以前「狀態」主要都是伺服器端在保存,但現在在現代的前端開發框架 React, Angular, Vue 中,越來越移到客戶端的瀏覽器中了。
## REST
REST (REpresentational State Transfer) 是一種分散式系統風格架構的 Web API 。
每一個 RESTful API 至少要遵循以下三個原則:
- 都像 HTTP ,屬於無狀態 (stateless)
- 支援一般的 HTTP verbs (GET, POST, PUT, DELETE, etc.)
- 以 JSON 或 XML 格式回傳資料
## Conclusion
一個 Web API 終究是很多端點的集合,來存取後面資料庫的資料。作為開發人員,我們控制 URLs 對每個端點時,判斷什麼資料可以提供、什麼方法可以進行等等。藉由 HTTP headers 我們可以設定很多階層的權限等等。
# Library Website and API
## Serializers
Serializer 將資料翻譯成 JSON 等格式然後顯示在一個 API 端點中。
# Todo API
在接下來的兩章,我們將會建立一個 Todo API 後端,並與 React 前端連結。
## Django REST Framework
Django REST Framework 有一長串隱藏式預設的設定,請見 [API Reference](https://www.django-rest-framework.org/api-guide/settings/#api-reference) 。
學習DRF預設的設定要花一些時間,在本書中我們會熟習某一些。要記得的重點是這些隱含的預設設定是設計給開發人員,可以快速的在本地開發端開始作業。對於上線的產品是不適合的,需要做一些調整。
在本章前端以 React 來寫,所以不做網頁,所以我們不需要創建任何 `template` 的檔案以及傳統的 Django `views` 。
那在僅做 Web API 的條件下,我們會更新以下 3 個檔案將 database model 轉成 Web API :
- `urls.py`
- `views.py`
- `serializers.py`
## URLs
筆者喜歡先從 `urls.py` 著手,因為它是我們 API 端點的入口。
## Serializers
從 model 轉換資料成可以被 URLs 輸出的 JSON ,所以我們需要 serializer 。
寫法跟在傳統的 `model.py` 很相似,唯一要記得的就是傳統 model 會自己產 `id` ,但在 `serializers.py` 要記得加,稍後的 `detail view` 要使用。
## Views
傳統的 Django Views 是用來客製化「將什麼資料傳給 template」,但在 DRF 中則是傳送給序列化資料。
DRF views 的語法刻意與傳統的 Django views 非常相似。移轉了常用的 generic views 。
可以看見 `queryset` 和 `serializer_class` 重複性很高,後面會教如何使用 `viewsets` 和 `routers` 以較少的 code 處理這個問題。
## Consuming the API
開發人員常會使用 [cURL_Client URL](https://en.wikipedia.org/wiki/CURL) 或 [HTTPie](https://httpie.org/) 來存取資料。在 2012 年 [Postman](https://www.getpostman.com/) 第三方套件發行,可以對 API 進行更豐富的操作。
## CORS
我們要處理的最後一步是 [Cross-Origin Resources Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) ,每當一個客戶端與不同網域或不同埠號的 API 互動,會產生資安風險。
具體來說, CORS 要求伺服器必須包含特定的 HTTP headers ,使客戶端可以決定「是否」以及「何時」允許跨域請求。
我們的 Django 後端會與專用的前端溝通,該前端在本地端開發時使用不同的埠號,部署後則在不同的則在不同的網域。
最簡單也是 Django REST Framework 推薦的方式是,使用基於我們的設定,自動包含適當的 HTTP headers 的中介軟體。
我們使用的套件是 [django-cors-headers](https://github.com/adamchainz/django-cors-headers) ,可以很容易的安裝到我們的專案。
在 `settings.py` 更新以下三個地方:
* add corsheaders to the INSTALLED_APPS
* add two new middlewares
* create a CORS_ORIGIN_WHITELIST
兩個中介軟體要在 MIDDLEWARE 設置的上方,很重要。 `localhost:3000` 是 React 預設的埠號,後期佈署前端應用程式之後也要將網域加入 `CORS_ORIGIN_WHITELIST` 。
延伸閱讀:「[深入認識跨域請求](https://www.ithome.com.tw/voice/129558)」
# Todo React Front-end
現在已經存在一個 API ,可以跟其他程式溝通了。本章會透過 React 使用 Todo API ,可以看見前後端一起運作的模樣。
這裡使用的 React 與其他前端框架 Vue, Angular, Ember 都適用, 這些都可以在 mobile apps 例如 iOS, Andriod, desktop apps 上運作,接後端的過程是十分類似的。
## Install Node
我們現在要設定 React app 當我們的前端。第一步是打開新的命令列,言下之意就是要保持兩個終端機介面。因為我們同時會需要後端 Todo 持續在本地伺服器運作。然後使用第二個終端機在不同埠號創建和運行 React 前端。這是在本地端模擬一個專用和部署 前/後 端的生產環境的樣子。
在新的命令列安裝 `NodeJS` (JavaScript runtime engine),使我們能夠在瀏覽器之外運行 JavaScript 。
```bash=
$ brew install node
```
## Install React
我們使用 `create-react-app` 套件來快速開啟新的 React 專案,可以使用一個指令產生專案樣板並將 dependencies 安裝完成。
使用 JavaScript 的套件管理員 npm 來安裝 React ,而在這裡我們會使用 npx ,是一個在本地端安裝套件的更好方式,不會污染全域空間。
```bash=
$ npx create-react-app frontend
$ cd frontend
$ yarn start
```
## Mock data
最終我們要直接使用 API ,但先複製資料到 React 的 src/App.js 是一個好的初始步驟,之後再配置 API 呼叫。
## Django REST Framework + React
現在要將我們的 Todo API 正式接上 React ,而非在 `list` 變數使用 mock data 。我們的 Django server 正在另一個命令列運作,而我們知道我們的端點是 `localhost:8000` ,所以我們必須發一個 `GET` 請求給它。
有兩個主要的方法來做 HTTP 的請求:
* [built-in Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
* [axios](https://github.com/axios/axios)
```bash=
$ npm install axios
```
# Blog API
* URLs - good practice to version your APIs
>> since when you make a large change there may be some lag time before various consumers of the API can also update. That way you can support a v1 of an API for a period of time while also launching a new, updated v2 and avoid breaking other apps that rely on your API back-end.
* Serializers - transform data into JSON & specify which fields to include/exclude
* Views - ListCreateAPIView for read and write / RetrieveUpdateDestroyAPIView for update and delete
# Permissions
* Django REST framework 的權限設定可以運作在 project-level, view-level, or at any individual model level
* one-line setting to add log in and log out to the browsable API:
path('api-auth/', include('rest_framework.urls'))
* view-level permission
- 每個 view 都要做
* project-level premission
- AllowAny
- IsAuthenticated
- IsAdminUser
- IsAuthenticatedOrReadOnly
* Custom permission
- 目標:作者只能修改或刪除自己的 post,其他人的 post 唯讀, superuser 例外
- 在 Django REST framework 中所有權限類別均繼承`類別 BasePermission`
- override the `has_object_permission` method
>> If you require object-level filtering of list views–for a collection of instances–you’ll need to filter by overriding the initial queryset.
* 一般的策略來說,在 project-level 可以制定較嚴格的權限,只讓有通過驗證的使用者看內容,而後在 view-level 和 custom permission 在特定需要的 API 端點制定適切的權限
# User Authentication
* authorization (權限): CRUD
* authentication (授權): register / sign-in / sign-out
* 由於 HTTP 是 stateless protocol (無狀態協議),所以 state 管理就變的更加重要
* Django API framework 預設是使用 session auth ,並透過 basic auth 在 HTTP HEADER 裡傳送 session ID
* 解決方案是對每個 HTTP request 傳送唯一的識別符,但並沒有一致統一 (agreed-upon) 的方式定義識別符的形式,所以 Django REST Framework 提供[四種](https://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication)內建的授權方式:
- **`Basic Authentication`** complete request/response flow:
- Client makes an HTTP request
- Server responds with an HTTP response containing a 401 (Unauthorized) status code and WWW-Authenticate HTTP header with details on how to authorize
- Client sends credentials back via the Authorization HTTP header. Example: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`
- Server checks credentials and responds with either 200 OK or 403 Forbidden status code
- **`Session Authentication`** is stateful because a record must be kept and maintained on both the server (the session object) and the client (the session ID), basic flow:
- A user enters their log in credentials (typically username/password)
- The server verifies the credentials are correct and generates a session object that is then stored in the database
- The server sends the client a session ID—not the session object itself—which is stored as a cookie on the browser
- On all future requests the session ID is included as an HTTP header and, if verified by the database, the request proceeds
- Once a user logs out of an application, the session ID is destroyed by both the client and server
- If the user later logs in again, a new session ID is generated and stored as a cookie on the client
- **`Token Authentication`** 是無狀態的,
- Client makes an HTTP request
- Server responds with an HTTP response containing a 401 (Unauthorized) status code and WWW-Authenticate HTTP header with details on how to authorize (token)
- A unique token is generated in client and then stored by the client as either a cookie or in local storage.
- This token is then passed in the header of each incoming HTTP request and the server uses it to verify that a user is authenticated.
:::info
**Cookies vs localStorge**
Cookies are used for reading server-side information. They are smaller (4KB) in size and automatically sent with each HTTP request.
LocalStorage is designed for client-side information. It is much larger (5120KB) and its contents are not sent by default with each HTTP request.
Tokens stored in both cookies and localStorage are vulnerable to XSS attacks. **The current best practice is to store tokens in a cookie with the httpOnly and Secure cookie flags.**
:::
| table | Basic Authentication | Session Authentication | Token Authentication|
| :-: | :- | :- |:- |
| pros | 簡易 | 1. client credential 只傳一次,較安全</br> 2.僅需 session 的 object/ID 匹配,較有效率|1. token 可以分享給多個前端,同個token可以代表同個user在網站及mobile app</br> 2.因為token在client,就沒有伺服器需要即時更新的問題
| cons | 1.每次request都要重複查找很沒效率</br> 2.明碼未加密|1. session ID 只在瀏覽器登入時有效,而且無法跨網域,所以 API 要支援多個前端 (比如一個網站一個mobile app) 就會產生問題</br>2.session object 在大型網站多個伺服器需要即時更新會有困難 </br> 3.針對每個 request 傳回 cookie 也是沒效率的 | 1. token 會越長越肥,且需針對request回傳,會產生效能問題
| notes |1.base64 encoded </br> 2.建議只能透過HTTPS使用 |不建議在有多個前端的 API 使用 | 近期較好的做法是將 token 與 httpOnly 和 Secure cookie flags 存在 cookie 中
* 內建的 token auth 沒有支援 token 期限的功能,但可以額外添加安全改善,針對一個使用者也只會產生一組 token
* 亦可參考 3rd-party packages JSON Web Tokens(JWTs), JWTs 可以產生唯一的 token 而且有期限功能
# Viewsets and Routers
* viewsets 和 routers 都是內建在 Django REST framework 可以加速開發 API 的工具,是在 views 和 urls 上新增的抽象層
# Schemas and Documentation
* A schema is a machine-readable document that outlines all available API endpoints, URLs, and the HTTP verbs (GET, POST, PUT, DELETE, etc.) they support.
* Documentation is something added to a schema that makes it easier for humans to read and consume.
* 本章目標:
- 使用 schema 和 documentation 方法
- 若現在或未來 API 有變更時,會自動更新的實作
* OpenAPI 與 CoreAPI 的不同(https://knivets.com/coreapi-vs-openapi/)
* [testing](https://www.django-rest-framework.org/api-guide/testing/)
# Packages
```bash=
(backend) $ pipenv install djangorestframework==3.9.2
```
```bash=
(backend) $ pipenv install django-cors-headers==2.5.2
```
```bash=
$ npx create-react-app frontend
```
```bash=
$ npm install axios
```