このページでは、Django での Web API の開発について解説していきます。
下記ページの前回の Django 入門 の連載で、ウェブブラウザからでなくても、ウェブアプリが HTTP リクエストを送信することで操作可能であることを説明しました。
【Django入門18】ウェブアプリのHTTPリクエストでの操作ただし、HTTP リクエストの送信でウェブアプリの操作は実現可能ではあるものの、通常のウェブアプリではウェブブラウザから操作されることを前提としており取得できるデータが HTML となるため、取得後のデータの扱い方が複雑になってしまうという課題がありました。
このページでは、ウェブブラウザ以外からの操作も容易とするため、ウェブブラウザ以外からの操作を前提とした Web API の開発を行っていきたいと思います。
このページでは、Django のみを利用して Web API を開発していきますが、Django REST Framework (DRF) を利用することで、より簡単に Web API を開発することもできます。ただし、今回は、Django で開発するウェブアプリにおける Web API の仕組みや Web API を実現するにあたって必要な処理を理解していただくため、あえて DRF は利用せずに Web API を開発していきたいと思います。DRF については、次回の Django 入門 の連載で解説します。
Web API とは
最初に、簡単に Web API について解説していきます。
Web API
まず、API とは、アプリの機能(操作手段)を他のソフトウェア(プログラム・アプリ・システム等)に提供する仕組み・インターフェースのことを言います。さらに、Web API とは、HTTP リクエストの送信・HTTP リクエストの受信によって利用できるようにした API のことを言います。なります。
例えば、コメントの投稿 API を公開しておけば、そのコメントの投稿 API を実行するプログラムを開発することで、そのプログラムからコメントの投稿を実施することができるようになります。
ボディのデータフォーマットは解析が容易なものを選択
また、一般的には、Web API では、入力と出力、すなわち HTTP リクエストのボディと HTTP レスポンスのボディのデータフォーマットは共通となります。具体的には、JSON や XML が用いられます。これらのデータは解析が容易で、下記ページで必要だった HTML の解析のような複雑な処理無しに、必要なデータの抽出を行うことが可能です。
HTTP リクエスト・HTTP レスポンスの送受信によって機能を利用するという点は、ウェブブラウザからウェブアプリを操作するときと同様になります。ですが、通常のウェブアプリでは、ウェブブラウザから利用されることを前提として開発されることが一般的ですので、ウェブアプリに送信する HTTP リクエストのボディは「URL エンコード」というデータフォーマットで、ウェブアプリから返却する HTTP リクエストのボディは「HTML」となっており、ウェブブラウザにとって都合の良いデータフォーマットでデータのやり取りが行われることになります。
ですが、これは前回の連載の中でも実感していただいたとおり、これだとウェブブラウザ以外からウェブアプリの機能の利用・操作が困難となります。なので、ウェブアプリに別途追加する形で Web API を開発し、その Web API と利用側との間でやり取りするデータを解析が容易な JSON や XML とすることで、ウェブブラウザ以外からも機能を利用しやすいウェブアプリに仕立てることが可能となります。
Web API の開発の仕方
先ほどもサラッと説明しましたが、基本的には Web API は既存のウェブアプリに追加する形で開発することになります。したがって、Web API を開発することで、ウェブアプリの機能をウェブブラウザと Web API の両方から利用することが可能となります(つまり、ウェブアプリをウェブブラウザと Web API の両方から操作することが可能)。
で、これも前述で解説した通り、Web API もウェブアプリと同様に、HTTP リクエスト・HTTP レスポンスの送受信によって機能・操作手段を提供するものであり、Django でのこれらの開発の仕方もほとんど同じです。ただし、Web API の場合、入力と出力のデータ(HTTP リクエストのボディと HTTP レスポンスのボディ)の形式が今までと異なるので、データの取得の仕方やデータの返却の仕方も異なることになります。
また、Web API は、既存のウェブアプリと共通のデータベースのテーブルを利用することが多いです。既存のウェブアプリと同じテーブルに対して操作を行うように Web API を開発することで、ウェブブラウザ or Web API からのテーブルの操作結果が他方側が利用するテーブルにも反映されるようになります。それにより、例えばウェブブラウザから投稿されたコメントを、Web API から取得するようなことも可能となります。
なので、Web API を既存のウェブアプリに追加する場合、基本的にはモデルクラスの定義は不要です。さらに、フォームを利用しないためフォームクラスの定義も不要で、HTTP レスポンスのボディが HTML ではないのでテンプレートファイルも不要となります。したがって、Web API は、Web API 用のビューを追加し、さらに、そのビューと URL とのマッピングを行うのみで開発することが可能です。
つまり、Web API は、今まで学んできたウェブアプリと同様の手順で、しかも views.py
と urls.py
の変更のみで開発することができます。なので、ウェブアプリの開発の仕方を理解していれば、簡単に開発することが可能です!
このあたりの Web API の開発の手順については、後述の API の開発 で詳細を解説ていきます。
API 仕様書の必要性
この Web API は、もちろん開発者自身が利用することもあるのですが、基本的には他のユーザーに利用してもらうことを目的に開発することになります。そして、Web API を他のユーザーに利用してもらうためには、Web API の使い方を他のユーザーに理解してもらう必要があります。
そのため、Web API を開発する場合、単に Web API を開発するだけでなく、その使い方をユーザーに理解してもらうための API 仕様書の作成が必要となります。そして、この API 仕様書で、各種 Web API を利用するために送信すべき HTTP リクエスト、さらには Web API から返却される HTTP レスポンスの詳細を説明します。
このような仕様書があれば、前回の連載で行ったような送信すべき HTTP リクエストの自身での調査が不要になります。そして、API 仕様書を読めば、送信すべき HTTP リクエストや受信した HTTP レスポンスの扱い方を理解することができ、誰でも API を利用するソフトウェアを開発することができるようになります。
逆に言うと、この仕様書が無いと、Web API の使い方が分からず、せっかく Web API を開発しても他のユーザーに利用してもらえない可能性もあるので注意してください。
ウェブブラウザからウェブアプリを操作する場合、視覚的にリンクやフォーム・ボタン等を確認することができるため、使い方を知らなくても直感的に操作することが可能です。ですが、Web API の場合は、送信する HTTP リクエストのデータの内容(URL・メソッド・ボディなど)によって機能の利用の可否が決まるので、送信すべきデータの内容を知らないと使用することができません。
もちろん、ウェブアプリの仕様や使い方もドキュメント化されていることに越したことはないのですが、Web API の場合は、利用してもらうためには仕様書の作成が必須であることは覚えておきましょう。どういった仕様書を作成すべきかについては、後述の API の仕様の定義 で解説していきます。
スポンサーリンク
Web API を開発するメリット
さて、この Web API をわざわざ開発することにはどういったメリットがあるのでしょうか?
その点について解説していきます。
ウェブブラウザ以外からの操作が容易になる
まず、ウェブブラウザ以外からのウェブアプリの機能の利用・操作が容易になる点が挙げられます。
下記ページで説明したように、Web API を開発しなくても、ウェブブラウザから送信される HTTP リクエストと同様のデータを他のソフトウェアから送信すれば、ウェブブラウザ以外からのウェブアプリの操作を実現することは可能です。
【Django入門18】ウェブアプリのHTTPリクエストでの操作ですが、通常のウェブアプリは、ウェブブラウザから操作されることを前提としているため、ウェブブラウザ以外のソフトウェアからは送信する HTTP リクエストの作り方や受信した HTTP レスポンスの扱い方が複雑になります。
ですが、Web API の場合は、ウェブブラウザから利用されることに捉われることなく、利用者が扱いやすいように仕様を決めて開発することができるため、ウェブブラウザ以外のソフトウェアからの利用が容易になります。例えば、HTTP リクエスト・HTTP レスポンスのボディのデータフォーマットを JSON にしてやれば、多くのプログラミング言語で JSON を容易に解析することができるようになっているため、その分 API を利用するソフトウェアも簡単に開発することができます。
また、Web API では、各 API の使い方も API 仕様書として公開されるため、それに従った HTTP リクエストを送信するだけでウェブアプリの機能を利用することができるようになります。
そして、上記ページでも解説したように、ウェブブラウザ以外からウェブアプリの操作を行うことができれば、次のようなメリットが得られます。
- iPhone アプリ・Android アプリから操作可能なウェブアプリが開発可能
- UI (フロントサイド) の開発の自由度が向上
- ウェブアプリの動作確認の自動化が容易
他のアプリと連携できる
基本的には、ウェブアプリは独立した存在であり、単独で動作することになります。ですが、Web API を公開することで、他のアプリやシステム等との連携動作を実現することが可能となります。
ここまでの説明の通り、API とは、アプリ・システム等の他のソフトウェアに機能を提供する仕組みです。つまり、この API を利用することで、他のソフトウェアが、あなたが開発したウェブアプリの機能を利用することができることになります。また、その機能がウェブアプリからデータを取得するものであれば、そのデータを他のソフトウェアに共有することもできることになります。
例えば、あなたが開発したウェブアプリが「料理レシピの登録機能」と「料理レシピのテキストでの取得機能」を備えているとしましょう。さらに、このウェブアプリは「レシピの取得 API」を公開しているとします。
さらに、他のウェブアプリが「テキストから動画を自動生成する機能」を備えている場合、あなたが開発したウェブアプリの Web API を利用し、取得したレシピのテキストから動画を生成することで、「投稿済みの料理レシピを動画として公開するサービス」が新たに生まれることになります。
このように、Web API を開発しておけば、その Web API を他のアプリが利用することで、複数のアプリが連携して動作することが可能となります。
新たな価値・サービスが生まれる可能性がある
そして、このアプリの連携により、先ほど紹介した「投稿済みの料理レシピを動画として公開するサービス」のような、新たな価値を持つサービスが生まれる可能性もあります。
これは、もちろんユーザーにもメリットがありますし、このサービスを提供するアプリだけでなく、そのアプリに機能を提供している「あなたが開発したウェブアプリ」の存在意義・知名度のアップにもつながることになります。
また、Web API を有料化すれば、Web API を利用してもらうだけで収益を得るようなことも可能となります。
このように、Web API を公開して他のソフトウェアと連携して動作できるようにしておくことで、新たな価値・サービスが生まれる可能性がありますし、それによってユーザーだけでなく、その Web API 開発者にとってもメリットを得ることができます。
また、このページでは、あくまでも「Web API を公開する側のウェブアプリ」の視点での解説を行っていきますが、もちろんあなたが開発したウェブアプリから他のウェブアプリの Web API を利用することも可能です。そして、それによって新たな価値・サービスを生み出すことができる可能性もあります。是非、Web API を利用することで他のソフトウェアと連携動作できること、さらに、その連携動作によって得られるメリットについては理解しておきましょう!
だいたい、Web API のイメージは湧いてきたでしょうか?
以降では、Web API を開発する手順について解説していきます。ここからは、Web API のことを単に「API」と呼ばせていただきますので、その点にはご注意ください。
API の仕様の定義
API を開発するためには、まず API 仕様を定義し、それを API 仕様書としてドキュメント化しておく必要があります。
この API 仕様を定義する目的は、大きく分けて下記の2つとなります。
- API 開発者に開発すべき API を示す
- API 利用者に API の使い方を示す
どちらも重要ですが、API を開発したとしても利用してもらえなければ意味がないため、特に後者の目的が重要になると思います。なので、API の仕様は、API 利用者に使い方が伝わるように定義し、それをドキュメント化しておくことが重要です。
ここからは、どういった項目に対して、どのような仕様を定義して仕様書に記載していく必要があるのか?という点について、実例も踏まえながら解説を行っていきます。そして、ここで定義した API の仕様に基づき、次の API の開発 の章で実際に API の開発を行っていきます。
また、ここでは、models.py
に下記の Comment
のモデルクラスが定義されていることを前提に、「コメントの投稿 API」「コメントの詳細取得 API」「コメントの一覧取得 API」の仕様の定義例を示していきます。
from django.db import models
class Comment(models.Model):
author = models.CharField(max_length=32)
text = models.CharField(max_length=256)
date = models.DateTimeField(auto_now_add=True)
URL とメソッド(エンドポイント)
API においては、HTTP リクエストの URL とメソッドによって実行する API を指定することが一般的です(さらにボディによって、必要なデータを送信する)。
そのため、各 API に対して URL とメソッドを割り当て、それを仕様として記載しておく必要があります。
この URL とメソッドは、操作対象のリソースの種類を「URL」で示し、さらに、そのリソースに対する操作の種類を「メソッド」で示すように、各 API に割り当てるのが分かりやすいと思います。
URL
先ほど説明したように、URL は API での操作対象のリソースの種類に基づいて割り当てるのが分かりやすいと思います。 ウェブアプリで操作対象となるリソースはデータベースのテーブル(モデルクラス)であることが多いため、モデルクラス名に基づいて URL を決めると API 仕様として分かりやすくなると思います。具体的には、モデルクラス名を小文字にし、さらに複数形にしたものを API の URL に割り当てると分かりやすいです。
例えば、今回公開しようとしている API では操作対象のリソースは Comment
となるため、URL としては /comments/
や /api/comments/
を割り当てると、操作対象のリソースが URL から一目瞭然になります。
メソッド
また、前述の通り、API には、その API で実施される操作の種類に応じてメソッドを割り当てることになります。この操作の種類とは、具体的には下記ページで解説した CRUD 操作となります。
【Django入門16】CRUDの実現(Create・Read・Update・Delete)そして、これらの CRUD の操作の種類に応じて、下記のようにメソッドを割り当てることが多いです。このようにメソッドを割り当てれば、メソッドから「API の利用によって実施される操作」が理解しやすくなります。
- Create 操作 (新規登録):
POST
- Read 操作 (取得・参照):
GET
- Update 操作 (更新):
PUT
(orPATCH
) - Delete 操作 (削除):
DELETE
もちろん、全ての操作に対応する API を開発する必要はなく、利用者に提供したい操作に対応した API のみを開発するので問題ありません。
URL とメソッドの定義例
ここまでをまとめると、コメントの操作に関する API は、下記のように URL とメソッドを割り当てることで、利用者にとって使い方や意味合いの分かりやすい API 仕様とすることができます。{id}
部分には、操作対象となるコメント(レコード)のプライマリーキー(id
)を指定してもらうことを想定しており、詳細取得・更新・削除操作に関しては、こういったプライマリーキーで操作の対象となるコメント(レコード)を特定する必要があるというのがポイントになります。
- コメントの投稿 API:
POST /api/comments/
- コメントの一覧取得 API:
GET /api/comments/
- コメントの詳細取得 API:
GET /api/comments/{id}/
- コメントの更新 API:
PATCH /api/comments/{id}/
- コメントの削除 API:
DELETE /api/comments/{id}/
で、今回は「コメントの投稿 API」「コメントの一覧取得 API」「コメントの詳細取得 API」を開発していくことになりますので、それぞれの API に割り当てる URL とメソッドは下記のようになります。
- コメントの投稿 API:
POST /api/comments/
- コメントの一覧取得 API:
GET /api/comments/
- コメントの詳細取得 API:
GET /api/comments/{id}/
今までの URL・メソッドの割り当て方との違い
このような URL とメソッドの割り当て方は、下記ページで解説した CRUD 操作に対する URL・メソッドの割り当て方と異なる点に注意してください。
【Django入門16】CRUDの実現(Create・Read・Update・Delete)ここまでの Django 入門では、ウェブブラウザから使用されることを前提にウェブアプリを開発してきました。そのため、特にデータの送信は「フォームの表示」と「フォームからのデータの送信」の2段階の操作で実現されるようになっており、それぞれの操作に対して同じ URL を割り当て、前者の操作に対するメソッドには GET
、後者の操作に対するメソッドには POST
を割り当てるようにしてきました。
また、HTML のフォームからは、GET
or POST
のメソッドの HTTP リクエストしか送信できないという制約があります。そのため、ここまでの解説の中では、受け付けるメソッドは基本的に GET
or POST
であることを前提にウェブアプリを開発してきました。
ですが、API の場合は、事前にフォームを表示することなく、直接データを送信することが可能ですし、HTML のフォームから HTTP リクエストを送信するわけではないので、すべてのメソッドを利用することが可能です。そのため、上記のように同じリソースに対する HTTP リクエストの URL を統一し、各操作に対して異なるメソッドを割り当てることができます。そして、これによって、より分かりやすい URL とメソッドの割り当てが行えることになります。
スポンサーリンク
HTTP リクエストのボディの形式
また、データの送信操作を行う API を実行する際には、データを HTTP リクエストのボディとして送信することになります。
このボディの形式を知らないと、ユーザーは API を利用するソフトウェアを開発することができないため、各種 API に対して HTTP リクエストのボディの形式を定義し、それも API 仕様に記載しておく必要があります。
この「ボディの形式」とは、具体的には「データフォーマット(JSON・XML・HTML・CSV など)」と「データの構造」となります。
リクエストのデータフォーマット
このデータフォーマットとしては、まず HTTP リクエストのボディと HTTP レスポンスのボディとで共通にしておくことをオススメします。この方が API 利用者が API を利用しやすくなります。また、具体的なデータフォーマットとしては JSON がオススメです。
Python の場合であれば、json
ライブラリを利用することで、JSON と Python の基本的な型のデータ(辞書・リスト・文字列など)の相互変換を容易に行うことができます。また、他のプログラミング言語でも、その言語の基本的な型のデータと JSON との相互変換を容易に行えるようになっていることが多いです。そのため、JSON をボディのデータフォーマットにしておくと、ウェブアプリ側も API 利用側のソフトウェアも作りやすくなります。
そのため、このページではボディのデータフォーマットに JSON を採用することを前提に解説を進めます。
リクエストのボディのデータの構造
また、データフォーマットだけでなく、公開する各種 API に対し、必要に応じてデータの構造も定義し、それを仕様書に記載しておく必要があります。これらが記載されていないと、API 利用者はどんなボディの HTTP リクエストを送信すればよいのかが分かりません。
具体的には、下記のような内容を決め、それを API 仕様書に記載しておく必要があります。
- ボディに必要なフィールド
- 各種フィールドが必須かどうか
- 各種フィールドの値の型
- その他の制限事項
特に、レコードの新規登録・更新機能を提供する API の場合、API 利用者から、新規登録するレコード or 更新後のレコードの情報を指定してもらう必要があります。そのため、それらを指定できるように、HTTP リクエストのボディのデータの構造を定義する必要があります。
リクエストのボディの形式の定義例
例えば、今回開発する「コメントの投稿 API」「コメントの詳細取得 API」「コメントの一覧取得 API」におけるリクエストのボディの形式の定義例は下記となります。
このように HTTP リクエストのボディの形式を定義して仕様書に記載しておけば、特に「コメントの投稿 API」を実行する時に送信する HTTP リクエストのボディに author
フィールドと text
フィールドが必須で、それぞれに指定すべき値や、それぞれのフィールドの意味合いを理解することができるようになり、API を利用するソフトウェアが開発しやすくなります。
- コメントの投稿 API:
- リクエストのボディ (JSON)
Name (* 必須) Type Description author
*文字列
(文字数:1
~32
)投稿者名 text
*文字列
(最大文字数:1
~256
)本文
- リクエストのボディ (JSON)
- コメントの詳細取得 API:
- リクエストのボディ:なし
- コメントの一覧取得 API:
- リクエストのボディ:なし
クエリパラメーター
上記で示した例からも分かるように、メソッドが GET
の API の場合は HTTP リクエストでボディは無しとするのが一般的です。なので、メソッドが GET
の API で、何かしらのパラメーターを指定できるようにしたい場合は、ボディではなくクエリパラメーターを利用することになります。
クエリパラメーターとは、URL の末尾に追加することでサーバーに送信するデータとなります。クエリパラメーターは、?
から始まり、パラメーターを「キー名」と「値」のペアで指定します。例えば下記は、キー名が s
で値が django
のクエリパラメーターを指定する例になります。
https://daeudaeu.com/?s=django
複数のクエリパラメーターを指定することも可能で、その場合は、&
で各ペアを区切ることになります。
クエリパラメーターの利用例としては、レコードの一覧取得を行う API において、取得するレコードの条件を指定できるようにしたり、レコードの詳細取得を行う API において、HTTP レスポンスのボディに含めるフィールドを指定できるようにしたりするような例が挙げられます。
ボディ同様に、クエリパラメーターを指定可能な API を公開する場合、指定可能なクエリパラメーターを API 利用者に理解してもらうために、クエリパラメーターに関しても API 仕様書に記載しておく必要があります。
指定可能なクエリパラメーターの定義例
例えば、今回開発しようとしている「コメントの詳細取得 API」であれば、下記のように指定可能なクエリパラメーターの形式を定義して仕様書に記載しておけば、API 利用時のクエリパラメーターの指定の仕方を理解することができるようになります。
- コメントの詳細取得 API
- 指定可能なクエリパラメーター
Key Name Type Description min_id
整数 取得するコメントの範囲(開始 ID) max_id
整数 取得するコメントの範囲(終了 ID)
- 指定可能なクエリパラメーター
ステータスコード
また、API においても、当然エラーが発生して実行に失敗する可能性があります。API でエラーが発生する原因としては、下記のようなものが挙げられます。
- HTTP リクエストのボディが仕様を満たしていない
- 認証に失敗した
- URL で指定されたレコードが存在しない
- 許可されていない操作を行おうとしている
このようなエラーが発生したことを API 利用者に通知するため、HTTP レスポンスのステータスコードを「エラーの有無」や「エラーの原因」に応じて適切に設定することが必要となります。
ただし、API から適切なステータスコードの HTTP レスポンスを返却するようにしたとしても、API 利用者が各ステータスコードの意味合いを理解していなければ意味がないため、少なくとも各 API における成功時のステータスコードを定義し、それを API 仕様書に記載しておく必要があります。
また、API における HTTP レスポンスのステータスコードは、下記の基準で定義するので良いと思います。
- 成功時:
200
:取得・更新に成功した201
:新規登録に成功した204
:HTTP レスポンスのボディ無し(主に削除に成功した時)
- エラー発生時:
400
:HTTP リクエストのボディが不正である
(必須フィールドが存在しない・フィールドの値の型が異なるなど)401
:認証に失敗した403
:権限のない操作が行われた
(他の人が作成したレコードを削除しようとした、など)404
:操作対象のレコードが存在しない
ステータスコードの定義例
例えば、今回公開しようとしている API であれば、下記のように成功時のステータスコードを定義して仕様書に記載しておけば、API から返却された HTTP レスポンスのステータスコードより、API の成功・失敗を判断することができるようになります。
- コメントの投稿 API:
201
- コメントの詳細取得 API:
200
- コメントの一覧取得 API:
200
とりあえず、上記のように成功時のステータスコードを定義しておけば、API の成功・失敗は判断できるので、今回は、API でエラーが発生した時のステータスコードの定義は省略します。ですが、実際に API 仕様書を作成する場合は、API の実行に失敗した時のステータスコードに関しても記述しておいた方が API 利用者にとって親切だと思います。
スポンサーリンク
HTTP レスポンスのボディの形式
HTTP リクエストのボディと同様に、HTTP レスポンスのボディの形式に関しても定義を行い、それを API 仕様として記載しておく必要があります。
レスポンスのボディのデータフォーマット
HTTP レスポンスのボディのデータフォーマットとしては、基本的には HTTP リクエストと同じものを採用することをオススメします。その方が API の使い方が分かりやすいです。
今回は、HTTP リクエストのボディのフォーマットを JSON としていますので、HTTP レスポンスのボディのフォーマットも JSON として解説を進めていきたいと思います。
レスポンスのボディのデータの内容
また、レスポンスのボディのデータの構造に関しても API 仕様書に記載しておく必要があります。
特に、レコードを取得する API では、HTTP レスポンスのボディでレコードの各種フィールドの値を取得することになります。ですが、どういったフィールドが存在し、さらに、それらのフィールドの値がどういった型であるのかについて理解していないと、API 利用者はボディのデータを上手く扱うことができません。そのため、HTTP レスポンスのボディの構造の情報に関しても API 仕様として定義し、それを仕様書に記載しておく必要があります。
例えば、コメントの詳細取得 API であれば、レスポンスのボディに関して下記のような仕様が記載されていれば、受け取ったボディを JSON から変換した辞書には id
キー・author
キー・text
キー・date
キーが存在すること、さらに各キーの値の型や、それぞれのフィールドの意味合いも理解することができます。
- レスポンスのボディ
Name Type Description id
整数 コメントの ID author
文字列 投稿者名 text
文字列 本文 date
文字列
(ISO 8601 形式)投稿日時
また、コメントの一覧取得 API であれば、レスポンスのボディに関して下記のような仕様が記載されていれば、受け取ったボディを JSON からリストに変換することができ(JSON における配列は Python のリストに変換可能)、その各要素には id
キー・author
キー・text
キー・date
キーが存在することを理解することができます。
- レスポンスのボディ:下記オブジェクトを要素とする「配列」
Name Type Description id
整数 コメントの ID author
文字列 投稿者名 text
文字列 本文 date
文字列
(ISO 8601 形式)投稿日時
エラー時のレスポンスのボディの構造
さらに、API でエラーが発生した場合には、そのエラーの原因が API 利用者に理解できるように、HTTP レスポンスのボディでエラーの原因を示すことも重要となります。
ステータスコード で説明したように、API でエラーが発生した場合、エラーの原因に応じて HTTP レスポンスのステータスコードを設定することになります。
ですが、ステータスコードだけではエラーの原因の詳細までを示すことができないため、エラー発生時には HTTP レスポンスのボディでエラーの原因の詳細を伝えることが一般的です。特に、ステータスコードが下記の 400
である場合、ボディが不正であることがステータスコードから判断できたとしても、エラーが発生している具体的な箇所を突き止められない可能性があります。
400
:HTTP リクエストのボディが不正である
(必須フィールドが存在しない・フィールドの値の型が異なるなど)
例えば、コメントの投稿 API 実行時に下記のようなボディの HTTP リクエストが送信されてきたとします。これは、"author"
のスペルを間違っている例になります。
{
"auther": "Yamada Hanako",
"text": "Hello World"
}
つまり、このリクエストのボディには、必須フィールドである "author"
が存在しないため、仕様とは異なる形式のボディが送信されたことになります。で、こういったボディが不正な場合にはステータスコードに 400
をセットした HTTP レスポンスを返却することが一般的です。
なんですが、ステータスコード 400
の HTTP レスポンスを API 利用者が受け取ったとしても、そのステータスコードのみでは、送信したボディに問題があることが分かっても、具体的に問題のある箇所が特定できない or 特定に時間がかかる場合があります。上記の例であれば、"author"
のスペルを "auther"
であると思い込んでいる人であれば、ステータスコード 400
の HTTP レスポンスを受け取っても、ボディのどこを修正すればよいかが分からない可能性があります。得に、ボディの形式が複雑であれば、修正必要な箇所を特定するのが困難になります。
なので、API 実行時にエラーが発生した場合は、そのエラーの原因の詳細を HTTP レスポンスのボディで示すようにした方が良いです。
たとえば、下記のようなボディの HTTP レスポンスを受け取れば、author
フィールドが存在しないと指摘されているため、何が原因でエラーが発生し、どう修正すればよいかを API 利用者が推測しやすくなります。
{
"detail": "author field is required."
}
レスポンスのボディの形式の定義例
例えば、今回公開しようとしている API であれば、下記のように HTTP レスポンスのボディの形式を定義して仕様書に記載しておけば、API から受信した HTTP レスポンスのボディの扱い方を API 利用者が理解することができるようになります。
- コメントの投稿 API:
- HTTP レスポンスのボディ (JSON)
- 成功時:下記フィールドを持つオブジェクト
Name Type Description id
整数 コメントの ID author
文字列 投稿者名 text
文字列 本文 date
文字列
(ISO 8601 形式)投稿日時 - 失敗時:下記フィールドを持つオブジェクト
Name Type Description detail
文字列 エラーの原因
- 成功時:下記フィールドを持つオブジェクト
- HTTP レスポンスのボディ (JSON)
- コメントの詳細取得 API:
- HTTP レスポンスのボディ (JSON)
- 成功時:下記フィールドを持つオブジェクト
Name Type Description id
整数 コメントの ID author
文字列 投稿者名 text
文字列 本文 date
文字列
(ISO 8601 形式)投稿日時 - 失敗時:下記フィールドを持つオブジェクト
Name Type Description detail
文字列 エラーの原因
- 成功時:下記フィールドを持つオブジェクト
- HTTP レスポンスのボディ (JSON)
- コメントの一覧取得 API:
- HTTP レスポンスのボディ (JSON):
- 成功時:下記のオブジェクトを要素とする配列
Name Type Description id
整数 コメントの ID author
文字列 投稿者名 text
文字列 本文 date
文字列
(ISO 8601 形式)投稿日時 - 失敗時:下記フィールドを持つオブジェクト
Name Type Description detail
文字列 エラーの原因
- 成功時:下記のオブジェクトを要素とする配列
- HTTP レスポンスのボディ (JSON):
認証
また、API は誰もが利用できるように公開するのではなく、公開相手を制限することが多いです。この公開相手の制限は「認証」によって実現することが可能です。通常のウェブアプリでもパスワード認証等を実施し、ユーザー名やパスワードが不正である場合にウェブアプリを利用不可とすることが多いと思いますが、それと同様のことを API でも実現していく必要があります。
この認証の方式には様々なものが存在し、下記のようなものが認証方式の代表例となります。
- ベーシック認証
- Digest 認証
- セッション認証
- トークン認証
こういった認証を API で実施する場合、その認証方式についても API 仕様書に記述しておく必要があります。
次の API の開発 の章では、ここで定義した API の開発手順を解説していくことになるのですが、初めて API を開発する方も多いと思いますので簡単な例で手順を解説していきたいと思います。そのため、今回開発する API に関しては、認証方式は「なし」としたいと思います。
掲示板アプリで API を公開する においては、API の認証方式として「セッション認証」を採用した API の例を示していきます。
また、次回の連載においては、Django REST Framework を利用し、トークン認証を認証方式とした API の公開手順を解説していきます。
スポンサーリンク
API 仕様の定義のまとめ
とりあえず、ここまで解説してきたような項目を仕様書に記載しておけば、API 利用者が API 実行のために送信すべき HTTP リクエストの作り方や、返却されてきた HTTP レスポンスの扱い方を理解することができると思います。
また、API 仕様書では、単に仕様を説明するだけでなく、API を実行するサンプルスクリプトなどを載せておくのも親切だと思います。
とにかく、API 仕様書は、API 利用者の方に API の使い方を理解してもらえるように作成することを心がけましょう!
API の開発
続いて、API の開発手順について解説していきます。
ここでは、開発手順を示しながら、先ほど仕様を定義してきた下記の3つの API を実際に開発していきます。
- コメントの投稿 API
- コメントの詳細取得 API
- コメントの一覧取得 API
これらの API の仕様に関しては、前述の API の仕様の定義 の節で説明していますので、仕様に関して思い出したい方は API の仕様の定義 を参照してください。
また、API は、Web API の開発の仕方 でも簡単に説明したように、既存のウェブアプリに対して API を追加する形で開発を進めることが多いです。ここではゼロベースから API を開発していきますが、既存のウェブアプリに対して API を追加するのであれば、API 開発:ビューの開発 の節からの手順で開発を進めていけば良いことになります。
API 開発の流れ
まず、API の開発手順の概要を説明しておきます。
簡単に言ってしまえば、API 開発で必要となるのは「各種 API の機能を提供するビューの追加」と「追加したビューと API に割り当てられた URL とのマッピング」の2つのみとなります。前者に関しては、views.py
の実装、後者に関しては urls.py
の実装によって実施していくことになります。そして、これらの views.py
の実装の仕方や urls.py
の実装の仕方に関しては、通常のウェブアプリと同様になります。なので、API を開発するからといって特に身構える必要はありません。
ただし、既存のウェブアプリの views.py
に API 関連のビューを追加していくと views.py
のコード量が膨れ上がってメンテナンス性が下がる可能性があるため、API は、既存のウェブアプリのプロジェクトに新たな「API 専用のアプリ」を追加して開発していくことが多いです。
つまり、API は、API 専用のアプリの views.py
に API 用のビューを定義し、urls.py
で、各種 API に割り当てた URL と views.py
で定義した API 用のビューとをマッピングすることで API を開発していくことになります(必要に応じて mixins.py
等のファイルを作成してクラス等を定義しても問題ありません)。そして、これにより、ウェブアプリから API が公開されることになり、他のソフトウェアやユーザーから API が実行できるようになります。
API 専用のアプリの views.py
からはモデルクラスの利用も必要となりますが、これに関しては API 専用のアプリに追加するのではなく、既存のアプリの models.py
に定義されたモデルクラスを利用することになります。また、API ではフォームを利用しないのでフォームクラスの定義も不要で、さらに HTTP レスポンスのボディの形式は HTML ではないのでテンプレートファイルも不要となります。
なので、API の開発は、通常のウェブアプリに比べると楽に行うことができます。
ただし、ウェブアプリを開発する時に利用した CreateView
や DetailView
のような、API 開発に適した View のサブクラス
が Django 自体には存在しません。そのため、Django での API の開発では、クラスベースビューで API を開発したとしてもビューの実装量は多くなります。ですが、次の連載で紹介する Django REST Framework を利用することで API 開発に適したクラスを利用することができるようになって開発効率を大幅に向上させることが可能となります。
ということで、ここから API の開発手順を示していきますが、ここで示す手順で API を開発する機会は少なく、結局 Djnago REST Framework を利用して、もっと簡単な手順で API を開発することの方が多いと思います。ただ、API を実現するために必要な処理や API の仕組みに関しては、ここで示す手順で開発した方が理解しやすいと思います。そして、そういった知識が今後 API を開発する時や API 開発時にトラブルが発生したときに役立つことも多いと思いますので、まずは Django のみでの API の開発の仕方を理解していきましょう!
スポンサーリンク
準備:プロジェクト・アプリの作成
では、API の開発を行っていきます。
基本的には、いつもウェブアプリを開発しているときと同じ手順でプロジェクトとアプリを作成していくことになります。
ただ、前述の通り、API は、既存のウェブアプリに対して追加する形で開発することが多いので、今回は1つのプロジェクトに2つのアプリを作成し、一方のアプリを既存の掲示板アプリのダミー(モデルクラスを定義するだけ)として、もう一方のアプリで掲示板アプリの機能を提供する API を開発していきます。
そのために、まずはコンソールアプリを起動し、適当なフォルダで下記コマンドを実行して api_test
というプロジェクトを作成してください。
% django-admin startproject api_test
このコマンドの実行によって api_test
フォルダが作成されるので、その api_test
プロジェクトのフォルダ内に移動後、次は下記コマンドを実行して forum
というアプリを作成します。
% python manage.py startapp forum
さらに、下記コマンドを実行して api
というアプリを作成します。
% python manage.py startapp api
続いて、今いる api_test
フォルダの中に、もう1つ api_test
フォルダが存在するはずなので、そのフォルダの中の settings.py
を開き、下記のように INSTALLED_APPS
のリストの先頭に 'forum',
と 'api',
を追加して下さい。
INSTALLED_APPS = [
'forum',
'api',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
さらに、settings.py
と同階層に urls.py
が存在するはずなので、この urls.py
を下記のように変更してください。今回は、forum
アプリにビューは定義しないため、下記の urlpatterns
の2つ目の要素部分をコメントアウトしています。forum
アプリにビュー等を定義した場合にコメントアウトを外すことで、api
アプリで定義したビューだけでなく、forum
アプリで定義したビューも動作するようになり、ウェブアプリと API とを共存させることができるようになります。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# path('forum/', include('forum.urls')),
path('api/', include('api.urls'))
]
準備:モデルクラスの定義
次は、モデルクラスを定義していきます。
基本的には、API では、既存のウェブアプリ側で定義されたモデルクラスを利用する形で機能を実現していくことになります。今回の場合は forum
を既存のウェブアプリに見立てているため、forum
側の models.py
にモデルクラスを定義していきます。そして、このモデルクラスを利用することで、forum
アプリの機能を提供する API を開発していきます。
で、今回は、API の仕様の定義 でも示した下記の Comment
を forum/models.py
に定義することにしたいと思います。
from django.db import models
class Comment(models.Model):
author = models.CharField(max_length=32)
text = models.CharField(max_length=256)
date = models.DateTimeField(auto_now_add=True)
API 開発:ビューの開発
ここまでが準備で、ここからが API 開発の本番になります。
前述の通り、API 開発で主に必要となるのは「各種 API の機能を提供するビューの追加」と「追加したビューと API に割り当てられた URL とのマッピング」の2つです。そして、前者に関しては views.py
、後者に関しては urls.py
で実施していくことになります。
で、この views.py
の作り方なんですが、これに関しては、API の開発においても、基本的には通常のウェブアプリの時と同様となります。
具体的には、通常のウェブアプリのビューにおいても、API のビューにおいても、views.py
に下記のような流れで処理を行うクラス(のメソッド)や関数を定義していくことで開発していくことになります。
- リクエストのボディを取得する [必要に応じて]
- ボディのデータの各種フィールドに対して妥当性を検証する [必要に応じて]
- クエリパラメーターを取得する [必要に応じて]
- 提供する機能に応じた処理を実施する
- HTTP レスポンスを返却する
ただし、通常のウェブアプリのビューと API のビューとは入力と出力のデータ、すなわち HTTP リクエストのボディと HTTP レスポンスのボディのデータフォーマットが異なるため、HTTP リクエストのボディの取得の仕方や妥当性の検証の仕方(上記の 1. と 2.)、さらには返却する HTTP レスポンスの生成の仕方が異なることになります(上記の 5.)。
つまり、API のビューと通常のウェブアプリのビューとの開発の仕方の違いは「ボディの扱い方」のみとなります。それ以外の部分は、機能が同じであれば、API のビューと通常のウェブアプリのビューは全く同じ処理となります。
なので、基本的には通常のウェブアプリでのビューの開発の仕方を知っていれば、後はボディの形式に応じて処理を行うように変更することで API を開発することができます。
ということで、ここからは、特に通常のウェブアプリとのボディの扱い方の違いを意識しながら、具体的な API のビューの開発手順を解説していきます。
クラスを定義する
では、ここから、API のビューの開発手順を解説していきます。
まず、このページでは、API のビューはクラスベースビューでを開発していきたいと思います。
下記ページでも解説した通り、ウェブアプリの場合、役割に応じた View のサブクラス
が定義されており、それを継承するクラスを定義することで、簡単に様々な役割のビューを開発することができました。
ですが、API に適した View のサブクラス
が Django には存在しないため、Django のみを使用して API を開発する場合、ビューは View
というクラスを継承して定義していくことが多いです。この View
は、基本的には「HTTP リクエストのメソッド」に応じたクラスのメソッドの実行の振り分けの仕組みを提供するだけのクラスとなります。クラスの各メソッドは、開発者自身が定義する必要があります。
なので、View
を継承するメリットは、他の View のサブクラス
の継承する場合に比べると少ないです。ですが、それでも何も継承せずに関数ベースビューで開発していくよりかは楽になるので、View
を継承したクラスで API のビューを開発していきたいと思います。
ちなみに、Django REST Framework には API に適したクラスが定義されていますので、それを継承することで楽に API のビューを開発していくことが可能です。それに関しては、次回の Django 入門の連載で解説していきます。
ということで、api/views.py
を編集し、View
を継承した API 用のクラスを定義していきましょう!
今回は、下記のように api/views.py
を編集したいと思います。
from django.views.generic import View
class CommentAPI(View):
def get(self, request):
pass
def post(self, request):
pass
class CommentDetailAPI(View):
def get(self, request, id):
pass
上記では2つのクラスを定義しており、それぞれが下記の API に対応しています。
CommentAPI
:- コメントの投稿 API
- コメントの一覧取得 API
CommentDetailAPI
:- コメントの詳細取得 API
わざわざ2つのクラスを定義する必要があるのは、URL とメソッド(エンドポイント) で定義した API の URL に2種類のものが存在するからになります。具体的には、各 API の URL を下記のように定義しており、URL が /api/comments/
である API と、URL が /api/comments/{id}/
である API の2種類が存在します。
- コメントの投稿 API:
POST /api/comments/
- コメントの一覧取得 API:
GET /api/comments/
- コメントの詳細取得 API:
GET /api/comments/{id}/
urls.py
では、これらの URL とクラスをマッピングすることになるため、URL 毎にクラスを定義する必要があります。そのため、上記のように2つのクラスを定義することになります。
また、ここで定義した2つのクラスは両方とも View
を継承しているため、動作したときに HTTP リクエストのメソッドに応じて、クラスのメソッドが実行されることになります。具体的には、受け取った HTTP リクエストの「メソッドの名称を全て小文字に変換した名前」の View
のサブクラスのメソッドが実行されることになります。
ということで、api/views.py
を先ほどのように変更したことで、とりあえず HTTP リクエストのメソッドに応じてクラスのメソッドが実行される枠組みが完成したことになります。あとは、ビューとしては、各メソッドを作り込んで、API から機能を提供できるようにしていけば良いだけになります。
リクエストのボディを取得する
続いては、その各クラスのメソッドの実装を行っていきます。
まず、実施すべきことは、HTTP リクエストのボディの取得になります。
この HTTP リクエストのボディは、メソッドの引数 request
から取得可能です。より具体的には request.body
が受信した HTTP リクエストのボディのデータそのものとなります。
補足しておくと、通常のウェブアプリの場合、フォームから受信した URL エンコードされたリクエストのボディは request.POST
から辞書(のようなデータ)として取得することが可能です。ですが、それ以外の場合は、ボディを request.body
から取得する必要があります。API の場合もフォームを利用しない&ボディのデータフォーマットが異なるため、request.body
から取得する必要があります。
また、ボディが JSON の場合、json
モジュールの loads
関数によって、request.body
を Python のデータに変換することが可能です。今回開発するコメントの投稿 API の場合は、辞書に変換することができることになります。
そのため、HTTP リクエストのボディを JSON とする API のビューにおいては、下記のような、「request.body
を json.loads
によって Python のデータに変換する処理」が最初に必要となります。ただし、ボディを無しとする API では、この処理は不要となりますし、ボディのデータフォーマットが JSON 以外の場合は、そのフォーマットに合わせて処理を変更する必要があります。
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
ボディのデータが JSON かどうかの判断は、上記のように実際に json.loads
を実行することで確認可能です。json.loads
実行時に例外が発生した場合、受信した HTTP リクエストのボディが JSON として不正であることになります。そのため、この場合はエラーレスポンスを返却するようにする必要があります。API の仕様によってボディを JSON で定義していたとしても、利用者が必ず JSON のボディの HTTP リクエストを送信してくるとは限りませんので、上記のように、HTTP リクエストのボディに対して json.loads
を実行する時は、必ず例外のハンドリングも行うようにしましょう。
また、JsonResponse
クラスのコンストラクタを実行することで、第1引数で指定されたデータを JSON に変換したボディを持つ HTTP レスポンスを生成することができます。そして、それを return
することで、その HTTP レスポンスを API 利用者に返却することができます。また、status
引数により、HTTP レスポンスのステータスコードを指定することができます。
今回は HTTP レスポンスのボディのデータフォーマットは JSON として定義しています。なので、エラー発生時にもボディを JSON とする HTTP レスポンスを返却する必要があり、そのために JsonResponse
クラスのコンストラクタを実行する必要があるという点に注意してください。
ここまでの解説を踏まえると、下記のように api/views.py
を変更すれば、ビューがリクエストのボディを取得することができるようになることになります。メソッドが GET
の API ではリクエストのボディを無しと定義していますので、これらに対応する各種クラスの get
では、リクエストのボディの取得は不要となります。
import json
from django.views.generic import View
from django.http import JsonResponse
class CommentAPI(View):
def get(self, request):
pass
def post(self, request):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
class CommentDetailAPI(View):
def get(self, request, id):
pass
妥当性を検証する
次に行うのが、HTTP リクエストのボディの各種フィールドの妥当性の検証(バリデーション)になります。
API の仕様として、ボディのデータの構造を明確に定義していたとしても、それに従ったボディの HTTP リクエストを API 利用者が送信してきてくれるとは限りません。もしかしたら、悪意ある利用者がウェブアプリを攻撃するようなデータを送信してくる可能性もあります。なので、データの送信を受け付ける API では、必ず受信したデータの妥当性の検証を実施し、検証結果が OK の場合のみ、そのデータを扱うようにする必要があります。
また、フォームからデータが送信されてくる際には、フォームクラスの is_valid
メソッドによって妥当性の検証を実施することが可能です。ですが、API の場合はフォームを利用しないため、開発者自身がデータのチェック等の処理を実装して妥当性の検証する必要があります。ここが、送信されてきたデータを安全に扱う上で非常に重要となります。
ここで実施する妥当性の検証は、具体的には下記のような項目のチェックとなります。
- 必須フィールドが存在するか?
- そのフィールドの値は仕様通りの型か?
- そのフィールドの値は最大値や最大長等の仕様を満たしているか?
これらは API の仕様として定義しているはずなので、要は、その定義を満たしているかどうかを各フィールドに対してチェックすればよいことになります。
例えば、API の仕様の定義 で、コメントの投稿 API のボディのデータの構造を下記のように定義しています。
Name (* 必須) | Type | Description |
author * |
文字列 (文字数: 1 ~ 32 ) |
投稿者名 |
text * |
文字列 (文字数: 1 ~ 256 ) |
本文 |
なので、author
フィールド(author
キー)に関しては下記のようなチェックを行えば良いことになります。
author
フィールドが存在するかどうかauthor
フィールドの値の型が文字列であるかどうかauthor
キーの値の文字数が1
以上であるかどうかauthor
キーの値の文字数が32
以下であるかどうか
同様に、text
フィールド(text
キー)に関しても下記のようなチェックを行えば良いことになります。
text
フィールドが存在するかどうかtext
フィールドの値の型が文字列であるかどうかtext
キーの値の文字数が1
以上であるかどうかtext
キーの値の文字数が256
以下であるかどうか
これらの中で1つでも満たしていないものがあるのであれば、エラーの HTTP レスポンスを返却してメソッドを終了するようにしてやれば良いです。また、そのレスポンスのボディでエラーの原因を伝えるようにすると親切です。
このように、API の仕様をしっかり定義しておけば、妥当性の検証でチェックすべき項目も分かりやすいので、そういう意味でも API の仕様の定義が重要となります。
ということで、受信した HTTP リクエストのボディに対する各種フィールドの妥当性の検証を追加した api/views.py
は下記のようなものになります。メソッドが GET
の API ではリクエストのボディが無いため、妥当性の検証も不要です。
import json
from django.views.generic import View
from django.http import JsonResponse
class CommentAPI(View):
def get(self, request):
pass
def post(self, request):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'text' not in body:
return JsonResponse({'detail': 'textフィールドが存在しません'}, status=400)
if not isinstance(body['text'], str):
return JsonResponse({'detail': 'textフィールドの値には文字列を指定してください'}, status=400)
if len(body['text']) < 1 or len(body['text']) > 256:
return JsonResponse({'detail': 'コメントの文字列長が不正です'}, status=400)
if 'author' not in body:
return JsonResponse({'detail': 'authorフィールドが存在しません'}, status=400)
if not isinstance(body['author'], str):
return JsonResponse({'detail': 'authorフィールドの値には文字列を指定してください'}, status=400)
if len(body['author']) < 1 or len(body['author']) > 32:
return JsonResponse({'detail': '投稿者の文字列長が不正です'}, status=400)
class CommentDetailAPI(View):
def get(self, request, id):
pass
クエリパラメーターを取得する
また、API をクエリパラメータが指定できるように定義している場合、ビューでのクエリパラメーターの取得が必要となります。
ただし、クエリパラメーターの取得の仕方は通常のウェブアプリの時と同様です。具体的には、ビューの引数 request
に request.GET.get('パラメーター名')
を実行させることでクエリパラメーターを取得することができます。
今回は、コメントの一覧取得 API で下記のクエリパラメーターを指定できるようにしているため、request.GET.get('min_id')
と request.GET.get('max_id')
を実行してクエリパラメーターの値を取得すればよいことになります。
Key Name | Type | Description |
min_id |
整数 | 取得するコメントの範囲(開始 ID) |
max_id |
整数 | 取得するコメントの範囲(終了 ID) |
また、クエリパラメーターの型等のチェックも忘れずに行うようにしましょう。
クエリパラメーターの取得処理を追加した api/views.py
は下記のようなものになります。今回は、コメントの一覧取得 API 以外は指定可能なクエリパラメーターは無しという仕様にしているため、クエリパラメーターの取得はコメントの一覧取得 API に対してのみ必要となります。
import json
from django.views.generic import View
from django.http import JsonResponse
class CommentAPI(View):
def get(self, request):
# クエリパラメーターの取得
min_id = request.GET.get('min_id')
max_id = request.GET.get('max_id')
# 妥当性の検証
if min_id is not None:
try:
min_id = int(min_id)
except ValueError:
return JsonResponse({'detail': 'min_idには整数を指定してください'}, status=400)
if max_id is not None:
try:
max_id = int(max_id)
except ValueError:
return JsonResponse({'detail': 'max_idには整数を指定してください'}, status=400)
def post(self, request):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'text' not in body:
return JsonResponse({'detail': 'textフィールドが存在しません'}, status=400)
if not isinstance(body['text'], str):
return JsonResponse({'detail': 'textフィールドの値には文字列を指定してください'}, status=400)
if len(body['text']) < 1 or len(body['text']) > 256:
return JsonResponse({'detail': 'コメントの文字列長が不正です'}, status=400)
if 'author' not in body:
return JsonResponse({'detail': 'authorフィールドが存在しません'}, status=400)
if not isinstance(body['author'], str):
return JsonResponse({'detail': 'authorフィールドの値には文字列を指定してください'}, status=400)
if len(body['author']) < 1 or len(body['author']) > 32:
return JsonResponse({'detail': '投稿者の文字列長が不正です'}, status=400)
class CommentDetailAPI(View):
def get(self, request, id):
pass
提供する機能に応じた処理を実施する
ここまで追加してきた処理により、HTTP リクエストからボディやクエリパラメーター等のデータが取得でき、それらが仕様を満たしていることが確認できたことになりますので、後は API が提供する機能に応じた処理を追加していけばよいことになります。
この機能に応じた処理の実装に関しては、特に API であることは関係なく、通常のウェブアプリを開発する時と同じように実装すれば良いです。データベース操作に関しても、通常のウェブアプリと同じく、モデルクラスを利用して実施することが可能です。
ということで、ここに関しては特筆すべき内容もないので、いきなり実装例を示していきたいと思います。
各 API が提供する機能に応じた処理を追加した api/views.py
は下記のようなものになります。モデルクラス Comment
が forum
で定義されているため、forum.models
から import
する必要がある点に注意してください。
import json
from django.views.generic import View
from django.http import JsonResponse
from forum.models import Comment
class CommentAPI(View):
def get(self, request):
# クエリパラメーターの取得
min_id = request.GET.get('min_id')
max_id = request.GET.get('max_id')
# 妥当性の検証
if min_id is not None:
try:
min_id = int(min_id)
except ValueError:
return JsonResponse({'detail': 'min_idには整数を指定してください'}, status=400)
if max_id is not None:
try:
max_id = int(max_id)
except ValueError:
return JsonResponse({'detail': 'max_idには整数を指定してください'}, status=400)
# コメント一覧の取得
comments = Comment.objects.all()
if min_id is not None:
# idがmin_id以上のレコードに絞る
comments = comments.filter(id__gte=min_id)
if max_id is not None:
# idがmax_id以下のレコードに絞る
comments = comments.filter(id__lte=max_id)
def post(self, request):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'text' not in body:
return JsonResponse({'detail': 'textフィールドが存在しません'}, status=400)
if not isinstance(body['text'], str):
return JsonResponse({'detail': 'textフィールドの値には文字列を指定してください'}, status=400)
if len(body['text']) < 1 or len(body['text']) > 256:
return JsonResponse({'detail': 'コメントの文字列長が不正です'}, status=400)
if 'author' not in body:
return JsonResponse({'detail': 'authorフィールドが存在しません'}, status=400)
if not isinstance(body['author'], str):
return JsonResponse({'detail': 'authorフィールドの値には文字列を指定してください'}, status=400)
if len(body['author']) < 1 or len(body['author']) > 32:
return JsonResponse({'detail': '投稿者の文字列長が不正です'}, status=400)
# コメントの新規登録
comment = Comment()
comment.author = body['author']
comment.text = body['text']
comment.save()
class CommentDetailAPI(View):
def get(self, request, id):
# idが引数idと一致するレコードを取得
try:
comment = Comment.objects.get(id=id)
except Comment.DoesNotExist:
return JsonResponse({'detail': 'レコードが存在しません'}, status=404)
基本的には、各メソッドに対してデータベース操作を追加しただけの変更となります。
1点ポイントを挙げると、それは CommentDetailAPI
の get
メソッドで、テーブルに「id
が id
のレコード」が存在しない場合に単に例外を発生させるのではなく、わざわざ例外をキャッチして JsonResponse
のインスタンスを return
している点になります。
元々、ビューで例外を発生させると、その例外に応じた適切な HTTP レスポンスが Django フレームワークから返却されるようになっています。なので、通常のウェブアプリの場合は、単に例外を発生させるだけでも問題ありません。ですが、この場合、HTTP レスポンスのボディが HTML となってしまいます。
それに対し、今回開発する API ではエラー発生時にも JSON をボディとする HTTP レスポンスを返却する仕様としているため、ビューで例外を発生させてメソッドを終了するのではなく、その例外をキャッチし、JSON をボディとする HTTP レスポンスを返却するように実装する必要があります。
HTTP レスポンスを返却する
API が提供する機能に応じた処理が実装できれば、あとは、その処理の結果を HTTP レスポンスとして返却する処理を実装することで、API のビューが完成することになります。
ここで重要になるのが、API の仕様通りの形式のボディの HTTP レスポンスを返却する必要があるという点になります。
通常のウェブアプリの場合、HTTP レスポンスは render
関数の実行によって生成することが多いです。そして、この場合は、ボディを HTML とする HTTP レスポンスが生成されることになります。そして、その生成された HTTP レスポンスを return
することで、そのレスポンスをウェブブラウザに返却することができます。
それに対し、API の場合は、仕様として定義したデータフォーマットのボディの HTTP レスポンスを返却する必要があります。今回の場合は、ボディのデータフォーマットを JSON としていますので、ボディが JSON である HTTP レスポンスを返却する必要があります。また、この HTTP レスポンスのステータスコードは、API 仕様として定義したものである必要があります。
このような、ボディを JSON とする HTTP レスポンスは、ここまでの説明の中にも登場した JsonResponse
のコンストラクタの実行によって生成することが可能です。したがって、各種クラスのメソッドの最後では、下記のように引数指定を行った JsonResponse
のコンストラクタを実行し、その返却値を return
する処理が必要となります。
- 第1引数:JSON に変換したいデータ
status
引数:API 成功時のステータスコード
これだけの話ではあるのですが、この JsonResponse
の第1引数に指定するデータに関しては注意点があるので、その点について説明しておきます。
まず、JsonResponse
のコンストラクタの第1引数に指定可能なデータは限られており、辞書・リスト・タプル・文字列といった、基本的な Python のデータのみ指定可能です。特に注意が必要なのが、レコード(モデルクラスのインスタンス)そのものや、レコードを要素とする辞書やリスト等は指定できないという点になります。
したがって、レコードを API 利用者に返却するためには、そのレコードを一旦 JsonResponse
のコンストラクタの第1引数に指定可能なデータに変換する必要があります。そして、変換後のデータを第1引数に指定して JsonResponse
のコンストラクタを実行し、その返却値を return
することになります。また、基本的には、レコードは辞書に変換することになります。
このレコードの辞書への変換方法は複数存在するのですが、単純に、それぞれのキーと値を、レコードのフィールド名・フィールドの値とした辞書を生成することでも変換可能です。
例えば、今回定義している Comment
の場合、下記のような処理によってレコード(モデルクラスのインスタンス)の辞書への変換を行うことができることになります。
# comment:Commentのインスタンス
comment_dict = {
'id': comment.id,
'author': comment.author,
'text': comment.text,
'date': comment.date
}
ウェブアプリではデータベース操作を行うことが多く、HTTP レスポンスのボディにデータベースからの取得結果となるレコードをセットすることも多いです。そして、これは API に関しても同様のことが言えます。したがって、上記のようなレコードから辞書への変換が必要となる機会も多いです。なので、JsonResponse
のコンストラクタの第1引数にはレコードそのものは指定できないこと、さらにはレコードから辞書への変換方法はしっかり覚えておきましょう。
また、JsonResponse
のコンストラクタでは、デフォルトでは第1引数に辞書しか指定できないようになっているので、この点にも注意してください。第1引数に辞書以外を指定して JsonResponse
のコンストラクタを実行すると下記の例外が発生することになります。
TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False.
辞書以外を第1引数に指定したい場合は、JsonResponse
のコンストラクタに引数 safe=False
の指定が必要となります。
JsonResponse(辞書以外のデータ, safe=False)
このように、引数 safe=False
を指定することで、辞書以外のデータを JSON に変換した結果をボディとする HTTP レスポンスを返却することができるようになります。例えば、リストやタプル等を JSON に変換した結果をボディとする HTTP レスポンスも返却することが可能です。
ただし、リストやタプルの場合、それらの要素にレコードが存在すると JsonResponse
のコンストラクタでの JSON への変換に失敗することになるので注意してください。リスト・タプルの要素がレコードである場合、一旦すべての要素を辞書に変換し、その変換結果を要素とするリスト・タプルを新たに生成して JsonResponse
のコンストラクタの第1引数に指定する必要があります。
ここまでの解説を踏まえ、提供する機能に応じた処理を実施した後に HTTP レスポンスの返却処理を追加した api/views.py
は下記のようなものになります。
import json
from django.views.generic import View
from django.http import JsonResponse
from forum.models import Comment
class CommentAPI(View):
def get(self, request):
# クエリパラメーター取得
min_id = request.GET.get('min_id')
max_id = request.GET.get('max_id')
# 妥当性の検証
if min_id is not None:
try:
min_id = int(min_id)
except ValueError:
return JsonResponse({'detail': 'min_idには整数を指定してください'}, status=400)
if max_id is not None:
try:
max_id = int(max_id)
except ValueError:
return JsonResponse({'detail': 'max_idには整数を指定してください'}, status=400)
# コメント一覧の取得
comments = Comment.objects.all()
if min_id is not None:
# idがmin_id以上のレコードに絞る
comments = comments.filter(id__gte=min_id)
if max_id is not None:
# idがmax_id以下のレコードに絞る
comments = comments.filter(id__lte=max_id)
# レスポンスのボディにセットするデータをリストで作成
comment_list = []
for comment in comments:
comment_dict = {
'id': comment.id,
'author': comment.author,
'text': comment.text,
'date': comment.date
}
comment_list.append(comment_dict)
# リストをJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_list, status=200, safe=False)
#@method_decorator(csrf_exempt)
def post(self, request):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'text' not in body:
return JsonResponse({'detail': 'textフィールドが存在しません'}, status=400)
if not isinstance(body['text'], str):
return JsonResponse({'detail': 'textフィールドの値には文字列を指定してください'}, status=400)
if len(body['text']) < 1 or len(body['text']) > 256:
return JsonResponse({'detail': 'コメントの文字列長が不正です'}, status=400)
if 'author' not in body:
return JsonResponse({'detail': 'authorフィールドが存在しません'}, status=400)
if not isinstance(body['author'], str):
return JsonResponse({'detail': 'authorフィールドの値には文字列を指定してください'}, status=400)
if len(body['author']) < 1 or len(body['author']) > 32:
return JsonResponse({'detail': '投稿者の文字列長が不正です'}, status=400)
# レコードの新規登録
comment = Comment()
comment.author = body['author']
comment.text = body['text']
comment.save()
# レスポンスのボディにセットするデータを辞書で作成
comment_dict = {
'id': comment.id,
'author': comment.author,
'text': comment.text,
'date': comment.date
}
# 辞書をJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_dict, status=201)
class CommentDetailAPI(View):
def get(self, request, id):
# idが引数idと一致するレコードを取得
try:
comment = Comment.objects.get(id=id)
except Comment.DoesNotExist:
return JsonResponse({'detail': 'レコードが存在しません'}, status=404)
# レスポンスのボディにセットするデータを辞書で作成
comment_dict = {
'id': comment.id,
'author': comment.author,
'text': comment.text,
'date': comment.date
}
# 辞書をJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_dict, status=200)
ここで覚えておいていただきたいキーワードを1つ紹介しておきます。それは「シリアライズ」になります。シリアライズとは、複数の要素から構成されるデータを JSON のようなテキストもしくはバイト列のような階層を持たないフラットなデータに変換することを言います。この “複数の要素から構成されるデータ” には、辞書や Django におけるモデルクラスのインスタンス(レコード)が含まれます。
基本的には、通信で送受信可能なのは、こういったシリアライズされたデータのみとなります。なので、辞書やレコード等を通信で送信するためには事前にシリアライズが必要となります。上記のビューにおいては、このシリアライズは、前述のようなレコードからの辞書への変換や、JsonResponse
のコンストラクタの実行によって実施していることになります。
このシリアライズという言葉は、ウェブアプリの開発や通信を行うプログラムを開発する際によく耳にすることになると思いますので、是非覚えておきましょう!
以上で、API のビューに最低限必要な処理が実装できたことになります!
その他のカスタマイズ
ここまで説明してきた内容は、ビューに最低限必要な処理を実装する手順となります。他にも Mixin
の継承やデコレーターの適用等によって、通常のウェブアプリのビューと同様に API をカスタマイズするようなことも可能です。
例えば下記のように api/views.py
を変更してデコレーターを適用することで、API での CSRF 対策を無効化することが可能です。
import json
from django.views.generic import View
from django.http import JsonResponse
from forum.models import Comment
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@method_decorator(csrf_exempt, name='dispatch')
class CommentAPI(View):
def get(self, request):
# クエリパラメーター取得
min_id = request.GET.get('min_id')
max_id = request.GET.get('max_id')
# 妥当性の検証
if min_id is not None:
try:
min_id = int(min_id)
except ValueError:
return JsonResponse({'detail': 'min_idには整数を指定してください'}, status=400)
if max_id is not None:
try:
max_id = int(max_id)
except ValueError:
return JsonResponse({'detail': 'max_idには整数を指定してください'}, status=400)
# コメント一覧の取得
comments = Comment.objects.all()
if min_id is not None:
# idがmin_id以上のレコードに絞る
comments = comments.filter(id__gte=min_id)
if max_id is not None:
# idがmax_id以下のレコードに絞る
comments = comments.filter(id__lte=max_id)
# レスポンスのボディにセットするデータをリストで作成
comment_list = []
for comment in comments:
comment_dict = {
'id': comment.id,
'author': comment.author,
'text': comment.text,
'date': comment.date
}
comment_list.append(comment_dict)
# リストをJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_list, status=200, safe=False)
#@method_decorator(csrf_exempt)
def post(self, request):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'text' not in body:
return JsonResponse({'detail': 'textフィールドが存在しません'}, status=400)
if not isinstance(body['text'], str):
return JsonResponse({'detail': 'textフィールドの値には文字列を指定してください'}, status=400)
if len(body['text']) < 1 or len(body['text']) > 256:
return JsonResponse({'detail': 'コメントの文字列長が不正です'}, status=400)
if 'author' not in body:
return JsonResponse({'detail': 'authorフィールドが存在しません'}, status=400)
if not isinstance(body['author'], str):
return JsonResponse({'detail': 'authorフィールドの値には文字列を指定してください'}, status=400)
if len(body['author']) < 1 or len(body['author']) > 32:
return JsonResponse({'detail': '投稿者の文字列長が不正です'}, status=400)
# レコードの新規登録
comment = Comment()
comment.author = body['author']
comment.text = body['text']
comment.save()
# レスポンスのボディにセットするデータを辞書で作成
comment_dict = {
'id': comment.id,
'author': comment.author,
'text': comment.text,
'date': comment.date
}
# 辞書をJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_dict, status=201)
class CommentDetailAPI(View):
def get(self, request, id):
# idが引数idと一致するレコードを取得
try:
comment = Comment.objects.get(id=id)
except Comment.DoesNotExist:
return JsonResponse({'detail': 'レコードが存在しません'}, status=404)
# レスポンスのボディにセットするデータを辞書で作成
comment_dict = {
'id': comment.id,
'author': comment.author,
'text': comment.text,
'date': comment.date
}
# 辞書をJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_dict, status=200)
下記ページでも解説したように、Django で開発したウェブアプリでは CSRF 対策が施されており、データが安全でないと判断された場合にエラーが発生するようになっています(エラー示す HTTP レスポンスが返却される)。そのため、データが安全であるとウェブアプリに判断されるように HTTP リクエストを作成して送信する必要があります。
【Django入門18】ウェブアプリのHTTPリクエストでの操作ですが、上記のようにデコレーターを適用することで、その CSRF 対策を無効化することが可能です。上記の場合は、データの送信を受け付ける可能性のある CommentAPI
での CSRF 対策が無効化されることになります。
また、通常のウェブアプリの時と同様に、Mixin
を継承することでログインしていないユーザーからの API の実行を制限する(セッション認証を実施する)ようなことも可能となります。この例に関しては、後述の 掲示板アプリで API を公開する で紹介します。
こんな感じで、必要に応じてデコレーターの適用や Mixin
の継承を行って API のカスタマイズを行うことが可能ですので、必要に応じてデコレーターの適用及び Mixin
の継承を行うようにしましょう!
スポンサーリンク
API 開発:ビューと URL とのマッピング
ビューが完成したら、最後に urls.py
でビューと URL とのマッピングを行います。これにより、API が実行された時、すなわち、各種 API に割り当てた URL の HTTP リクエストを受け取った時にビューが動作し、API に応じた処理が実行されて HTTP レスポンスが返却されるようになります。
また、urls.py
の作り方に関しては、通常のウェブアプリと同じになります。
今回は、各種 API に下記のように URL を割り当てており、
- コメントの投稿 API:
POST /api/comments/
- コメントの一覧取得 API:
GET /api/comments/
- コメントの詳細取得 API:
GET /api/comments/{id}/
さらに、views.py
で下記のように各種 API 向けのクラス(メソッド)を定義しているため、
CommentAPI
のpost
:コメントの投稿 APICommentAPI
のget
:コメントの一覧取得 APICommentDetailAPI
のget
:コメントの詳細取得 API
api/urls.py
は下記のように作成すればよいことになります。api/urls.py
は自動では生成されないので、手動で作成してください。
from django.urls import path
from . import views
urlpatterns = [
path('comments/', views.CommentAPI.as_view()),
path('comments/<int:id>/', views.CommentDetailAPI.as_view())
]
ここまでの変更により、各種 API に割り当てた URL・メソッドの HTTP リクエストをウェブアプリが受け取ると、その API の機能を実装したメソッドが実行され、その結果が HTTP レスポンスとして API 利用者に返却されるようになります。
ということで、以上の変更によって API が完成したことになります!
スポンサーリンク
動作確認
最後に、開発した API の動作確認を行っておきましょう!
API を実行可能な状態にするためには、通常のウェブアプリと同様に、マイグレーションと開発用ウェブサーバーの起動が必要となります。
マイグレーションの実施
まずは、api_test
フォルダの中、つまり manage.py
が存在するフォルダで下記コマンドを実行してください。
% python manage.py makemigrations
% python manage.py migrate
開発用ウェブサーバーの起動
続いて、同じフォルダで下記コマンドを実行して開発用ウェブサーバーを起動してください。
% python manage.py runserver
API の実行
後は、API の仕様の定義 での定義を満たした HTTP リクエストを送信してやれば API を実行することができます。
この HTTP リクエストの送信は、下記ページでも解説した通り、Python であれば requests
ライブラリを利用することで簡単に実施できます。
下記は、requests
モジュールを利用して HTTP リクエストを送信して各種 API を実行する Python スクリプトの例となります。call_post_comment_api
では「コメントの投稿 API」、call_get_comment_list_api
では「コメントの一覧取得 API」、call_get_comment_detail_api
では「コメントの詳細 API」がそれぞれ実行されるようになっています。
また、下記の main
の中で実行している json()
は、HTTP レスポンスのボディを JSON から Python のデータ(辞書やリストなど)に変換するメソッドとなります。
import requests
import json
scheme = 'http://'
hostname = 'localhost:8000'
def call_get_comment_list_api(min_id=None, max_id=None):
# コメントの一覧取得APIのURLを作成
url = scheme + hostname + '/api/comments/'
# クエリパラメーターをURLの末尾に結合
if min_id is not None and max_id is not None:
url += '?min_id=' + str(min_id) + '&max_id=' + str(max_id)
elif min_id is not None:
url += '?min_id=' + str(min_id)
elif max_id is not None:
url += '?max_id=' + str(max_id)
# コメントの一覧取得APIを実行
response = requests.get(url=url)
return response
def call_get_comment_detail_api(id):
# コメントの詳細取得APIのURLを作成
url = scheme + hostname + '/api/comments/' + str(id) + '/'
# コメントの詳細取得APIを実行
response = requests.get(url=url)
return response
def call_post_comment_api(text):
# コメントの投稿APIのURLを作成
url = scheme + hostname + '/api/comments/'
# リクエストのボディを作成
data = json.dumps({
'author': 'YamadaHanako',
'text': text,
})
# リクエストのヘッダーを作成
headers = {
'Content-Type': 'application/json'
}
# コメントの投稿APIを実行
response = requests.post(
url=url,
headers=headers,
data=data
)
return response
def main():
# コメントを10個投稿
for i in range(10):
response = call_post_comment_api('Hello World ' + str(i))
# レスポンスのボディを出力
body = response.json()
print(body)
# エラー発生時はスクリプト終了
if response.status_code != 201:
return
# コメントの一覧を取得
response = call_get_comment_list_api(min_id=5, max_id=8)
# レスポンスのボディを出力
body = response.json()
print(body)
# エラー発生時はスクリプト終了
if response.status_code != 200:
return
# コメントの詳細を取得
response = call_get_comment_detail_api(1)
# レスポンスのボディを出力
body = response.json()
print(body)
# エラー発生時はスクリプト終了
if response.status_code != 200:
return
main()
上記スクリプトを実行すれば、まずコメントの投稿 API が10回実行され、10個分のコメントのデータベースへの新規登録が行われることになります。そして、API を実行するたびに、その API から返却された HTTP レスポンスのボディが出力されることになります。その出力結果は下記のようなものになるはずです。
{'id': 1, 'author': 'YamadaHanako', 'text': 'Hello World 0', 'date': '2024-12-15T11:34:42.975Z'} ~略~ {'id': 10, 'author': 'YamadaHanako', 'text': 'Hello World 9', 'date': '2024-12-15T11:35:01.609Z'}
続いて、コメントの一覧取得 API が実行され、事前に投稿されたコメントのうち、ID が 5
~ 8
のコメントのレコードが取得され、それらのレコードの情報をボディとする HTTP レスポンスを受け取ることになります。そして、上記スクリプトでは、そのボディを出力するようにしており、その出力結果は次のようなものになるはずです。
[{'id': 5, 'author': 'YamadaHanako', 'text': 'Hello World 4', 'date': '2024-12-15T11:34:51.242Z'}, {'id': 6, 'author': 'YamadaHanako', 'text': 'Hello World 5', 'date': '2024-12-15T11:34:53.316Z'}, {'id': 7, 'author': 'YamadaHanako', 'text': 'Hello World 6', 'date': '2024-12-15T11:34:55.383Z'}, {'id': 8, 'author': 'YamadaHanako', 'text': 'Hello World 7', 'date': '2024-12-15T11:34:57.463Z'}]
この取得するコメントの ID の範囲はクエリパラメーターで指定しており、このクエリパラメーターに指定する値(もしくは call_get_comment_list_api
の引数)を変更すれば、取得されるコメントの ID の範囲も変化することが確認できると思います。クエリパラメーターを指定しなかった場合は全てのコメントが取得されます。
最後に、コメントの取得取得 API が実行されて ID が 1
のコメントのレコードが取得され、そのレコードの情報をボディとする HTTP レスポンスを受け取ることになります。そのボディの出力結果は下記のようなものになるはずです。
{'id': 1, 'author': 'YamadaHanako', 'text': 'Hello World 0', 'date': '2024-12-15T11:34:42.975Z'}
取得するコメントのレコードの ID は URL で指定するようになっているため、その URL の ID 部分(もしくは call_get_comment_detail_api
の引数)を変更すれば、取得されるコメントも変化することが確認できると思います。
このような結果が得られたことから、今回開発した「コメントの投稿 API」でコメントの投稿(レコードの新規登録)が実現できており、さらに「コメントの一覧取得 API」や「コメントの詳細取得 API」で、投稿済みのコメントが取得できることを確認できたことになります。また、これらの API は、API の仕様の定義 で定義したとおりの HTTP リクエストを送信することで実行することができ、さらに、API から返却された HTTP レスポンスのボディのデータの構造も仕様通りとなっていました。したがって、API の仕様の定義 で定義したとおりの API が開発できていることが確認できたことになります。
ここまで説明してきた通り、API の仕様を定義すること、さらに、その仕様に合わせて API を開発すること、そして、開発した API が仕様通りに動作することを確認することが、API をウェブアプリに追加する上で重要なポイントとなります。
ちなみに、上記では、わざわざスクリプトを作成して API の動作確認を行いましたが、メソッドが GET
の API であればウェブブラウザで動作確認することも可能です。
例えば、開発用ウェブサーバーを起動した状態でウェブブラウザから下記 URL を開けば、下記における /api/comments/
部分を URL、さらにメソッドを GET
とした HTTP リクエストが送信されることになります。
http://localhost:8000/api/comments/
したがって、「コメントの一覧取得 API」が実行されることになります。そして、下図のように、実行された API から返却された HTTP レスポンスのボディがブラウザ上に表示されることになり、「コメントの一覧取得 API」の動作確認を行うことができます。
説明は省略しますが、「コメントの詳細 API」に関しても同様にして動作確認を行うことが可能です。ウェブブラウザで確認する方が楽なことも多いので、メソッドが GET
の API の動作確認はウェブブラウザでも実施可能であることは覚えておきましょう!
掲示板アプリで API を公開する
最後に、掲示板アプリで API を公開する例を示していきたいと思います。
この Django 入門 の連載の中では簡単な掲示板アプリを開発してきており、前々回の連載では下記ページで静的ファイルを扱えるように掲示板アプリの改良を行いました。
【Django入門17】静的ファイルの扱い方(画像・JS・CSS)今回は、上記ページの 掲示板アプリで静的ファイルを扱う で示したアプリに対して API を追加し、API からコメントの操作を行えるようにしていきたいと思います。
具体的には、まずコメントの操作を行う API として下記を追加していきます。
- コメントの投稿 API
- コメントの一覧取得 API
- コメントの詳細取得 API
- コメントの更新 API
- コメントの削除 API
ただし、掲示板アプリはログインしないと使用できないようになっているため、上記の API ではセッション認証を行うようにし、実行できる利用者を制限するようにしていきたいと思います。
そのため、このセッション認証を実現するため、下記のログイン関連の API も追加していきたいと思います(セッション認証に必要な情報はログイン時に発行される)。
- ログイン API
- ログアウト API
スポンサーリンク
掲示板アプリのプロジェクト一式の公開先
この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。
https://github.com/da-eu/django-introduction
また、前述のとおり、ここでは前回の連載の 掲示板アプリで静的ファイルを扱う で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。
https://github.com/da-eu/django-introduction/releases/tag/django-staticfile
さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。以降では、基本的には前回からの差分のみのコードを紹介していくことになるため、変更後のソースコードの全体を見たいという方は、下記からプロジェクト一式を取得してください。
https://github.com/da-eu/django-introduction/releases/tag/django-web-api
API の仕様を定義する
ウェブアプリに API を追加するためには、最初に API の定義を行う必要があります。
基本的には API の仕様の定義 で説明した各項目に対して、API 毎に仕様を決めていけば良いです。ただし、今回は、コメントの操作を行う API に関してはセッション認証を行うようにするため、その点についても仕様として明記しておいた方が良いと思います。
今回は、下記ページの内容を API の仕様とし、この仕様に基づいた API を開発していきたいと思います。
【Django】掲示板アプリの Web API 仕様【参考】API 用のアプリを作成する
では、ここから API を開発していきます。基本的には、API の開発 で示したものと同様の手順で API を開発していきます。
まずは、API 用のアプリを作成していきましょう!
この掲示板アプリのプロジェクトは test_project
となっていますので、test_project
フォルダ(上の階層の test_project
フォルダ)の中で下記コマンドを実行してください。
% python manage.py startapp api
これにより、test_project
フォルダの中に api
アプリが作成されることになります。
また、下の階層の方の test_project
フォルダの中に settings.py
が存在しますので、その settings.py
をエディター等で開き、INSTALLED_APPS
を下記のように変更してください('api',
を追加)
INSTALLED_APPS = [
'forum',
'api',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
さらに、settings.py
と同階層にある urls.py
を下記のように変更してください。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('forum/', include('forum.urls')),
path('api/', include('api.urls'))
]
これにより、api
フォルダの urls.py
も含めて URL とビューとのマッピングが行われるようになります。
スポンサーリンク
Mixin
の定義
ここからビューを開発していくことになるのですが、まずはビューで利用する Mixin
を定義していきたいと思います。
今回、コメントの操作を行う API ではセッション認証を行います。セッション認証とは、ログイン時にウェブアプリが発行したセッション ID による認証です。このセッション ID は、ログインに成功した場合の HTTP レスポンスのクッキーにセットされてクライアントに渡され、以降、そのセッション ID を含むクッキーをセットした HTTP リクエストを送信することで、セッション認証での認証に成功するようになります。
セッション ID が含まれない・セッション ID が間違っている・セッション ID の期限が切れているクッキーの HTTP リクエストを送信した場合は、セッション認証での認証に失敗することになります。
ウェブアプリで非ログインユーザーが閲覧不可のページでもログインすると閲覧できるようになるのは、ウェブブラウザがログイン時に取得したセッション ID を含むクッキーをセットした HTTP リクエストをウェブアプリに対して送信するようになっているからになります。
このようなセッション認証を API で実施するため、セッション認証用の Mixin
を定義したいと思います。ここで定義する Mixin
をコメントの操作を行う API のビューに継承させることで、これらの API が実行される時にセッション認証が実施され、セッション認証に成功した場合のみビューが実行されるようになります。逆に、セッション認証に失敗した場合は API の実行を拒否することができます。
このセッション認証を実施する Mixin
の例は、下記の LoginRequiredAPIMixin
となります。api
フォルダ内に mixins.py
を新規作成し、そのファイルに下記をコピペしてください。
from django.http import JsonResponse
class LoginRequiredAPIMixin:
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return JsonResponse({'detail': '認証に失敗しました'}, status=401)
return super().dispatch(request, *args, **kwargs)
クッキーに含まれる形でセッション ID が送信されてきており、さらに、そのセッション ID がウェブアプリが発行したものとして正しい場合、request.user.is_authenticated
は True
となり、それ以外の場合は False
となります。なので、上記のように request.user.is_authenticated
が False
の場合にエラーの HTTP レスポンスを返却するようにすることで、不正なセッション ID が送信されてきた場合の API の実行を拒否することができるようになります。そして、これがセッション認証となります。
また、上記のように dispatch
メソッドを定義しておくことで、この Mixin
を継承したクラスベースビューの dispatch
メソッドがオーバーライドされることになります。この dispatch
メソッドは、クラスベースビューのビューの中で最初に実行されるメソッドとなるため、各種 API に対応するメソッドが実行される前に、セッション認証が実施されることになります。
ビューの開発
次は、ビューを開発していきます。
前述の通り、下記ページに記載した API の仕様に基づいてビューを開発していくことになります。
【Django】掲示板アプリの Web API 仕様【参考】結論としては、api
フォルダの views.py
を下記のように変更することで、これらの仕様に基づいた API のビュー部分を開発することができます。動作確認を楽に行うために、その他のカスタマイズ で示した手順で CSRF 対策は無効化しています。
import json
from forum.models import Comment
from .mixins import LoginRequiredAPIMixin
from django.contrib.auth import login, logout
from django.views.generic import View
from django.http import HttpResponse, JsonResponse
from django.views import View
from django.contrib.auth import authenticate, login
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@method_decorator(csrf_exempt, name='dispatch')
class LoginAPI(View):
def post(self, request):
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'username' not in body:
return JsonResponse({'detail': 'usernameフィールドが存在しません'}, status=400)
if not isinstance(body['username'], str):
return JsonResponse({'detail': 'usernameフィールドの値には文字列を指定してください'}, status=400)
if 'password' not in body:
return JsonResponse({'detail': 'passwordフィールドが存在しません'}, status=400)
if not isinstance(body['password'], str):
return JsonResponse({'detail': 'passwordフィールドの値には文字列を指定してください'}, status=400)
# 認証
user = authenticate(request, username=body['username'], password=body['password'])
if user is not None:
# 認証に成功したのログインを実施
login(request, user)
return JsonResponse({'success': 'ログインに成功しました'}, status=200)
else:
return JsonResponse({'detail': 'ログインに失敗しました'}, status=400)
@method_decorator(csrf_exempt, name='dispatch')
class LogoutAPI(View):
def post(self, request):
logout(request)
return JsonResponse({'success': 'ログアウトに成功しました'}, status=200)
@method_decorator(csrf_exempt, name='dispatch')
class CommentAPI(LoginRequiredAPIMixin, View):
def get(self, request):
# クエリパラメーター取得
min_id = request.GET.get('min_id')
max_id = request.GET.get('max_id')
# 妥当性の検証
if min_id is not None:
try:
min_id = int(min_id)
except ValueError:
return JsonResponse({'detail': 'min_idには整数を指定してください'}, status=400)
if max_id is not None:
try:
max_id = int(max_id)
except ValueError:
return JsonResponse({'detail': 'max_idには整数を指定してください'}, status=400)
# コメント一覧の取得
comments = Comment.objects.all()
if min_id is not None:
# idがmin_id以上のレコードに絞る
comments = comments.filter(id__gte=min_id)
if max_id is not None:
# idがmax_id以下のレコードに絞る
comments = comments.filter(id__lte=max_id)
# レスポンスのボディにセットするデータをリストで作成
comment_list = []
for comment in comments:
comment_dict = {
'id': comment.id,
'user': comment.user.username,
'text': comment.text,
'date': comment.date
}
comment_list.append(comment_dict)
# リストをJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_list, status=200, safe=False)
def post(self, request):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'text' not in body:
return JsonResponse({'detail': 'textフィールドが存在しません'}, status=400)
if not isinstance(body['text'], str):
return JsonResponse({'detail': 'textフィールドの値には文字列を指定してください'}, status=400)
if len(body['text']) < 1 or len(body['text']) > 256:
return JsonResponse({'detail': 'コメントの文字列長が不正です'}, status=400)
# レコードの新規登録
comment = Comment()
comment.user = request.user
comment.text = body['text']
comment.save()
# レスポンスのボディにセットするデータを辞書で作成
comment_dict = {
'id': comment.id,
'user': comment.user.username,
'text': comment.text,
'date': comment.date
}
# 辞書をJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_dict, status=201)
@method_decorator(csrf_exempt, name='dispatch')
class CommentDetailAPI(LoginRequiredAPIMixin, View):
def get(self, request, id):
# idが引数idと一致するレコードを取得
try:
comment = Comment.objects.get(id=id)
except Comment.DoesNotExist:
return JsonResponse({'detail': 'レコードが存在しません'}, status=404)
# レスポンスのボディにセットするデータを辞書で作成
comment_dict = {
'id': comment.id,
'user': comment.user.username,
'text': comment.text,
'date': comment.date
}
# 辞書をJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_dict, status=200)
def patch(self, request, id):
# ボディのデータの取得
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'detail': '無効なJSON形式です'}, status=400)
# 妥当性の検証
if 'text' not in body:
return JsonResponse({'detail': 'textフィールドが存在しません'}, status=400)
if not isinstance(body['text'], str):
return JsonResponse({'detail': 'textフィールドの値には文字列を指定してください'}, status=400)
if len(body['text']) < 1 or len(body['text']) > 256:
return JsonResponse({'detail': 'コメントの文字列長が不正です'}, status=400)
# idが引数idと一致するレコードを取得
try:
comment = Comment.objects.get(id=id)
except Comment.DoesNotExist:
return JsonResponse({'detail': 'レコードが存在しません'}, status=404)
if request.user != comment.user:
# 他のユーザーのレコードは更新不可
return JsonResponse({'detail': '他のユーザーのレコードは更新できません'}, status=403)
# レコードの更新(部分的な更新)
comment.text = body['text']
comment.save()
# レスポンスのボディにセットするデータを辞書で作成
comment_dict = {
'id': comment.id,
'user': comment.user.username,
'text': comment.text,
'date': comment.date
}
# 辞書をJSONに変換したデータをボディとするレスポンスを返却
return JsonResponse(comment_dict, status=200)
def delete(self, request, id):
# idが引数idと一致するレコードを取得
try:
comment = Comment.objects.get(id=id)
except Comment.DoesNotExist:
return JsonResponse({'detail': 'レコードが存在しません'}, status=404)
if request.user != comment.user:
# 他のユーザーのレコードは削除不可
return JsonResponse({'detail': '他のユーザーのレコードは削除できません'}, status=403)
# レコードの削除
comment.delete()
# ボディ無しのレスポンスを返却
return HttpResponse(status=204)
この views.py
ではクラスを4つ定義しており、それぞれのクラスの各メソッドで下記の API が提供する機能の実装を行っています。
LoginAPI
:post
:ログイン API
LogoutAPI
:post
:ログアウト API
CommentAPI
:get
:コメントの一覧取得 APIpost
:コメントの投稿 API
CommentDetailAPI
:get
:コメントの詳細取得 APIpatch
:コメントの更新 APIdelete
:コメントの削除 API
各メソッドの実装の仕方・ビューの開発の仕方に関しては、基本的に API の開発 で解説していますので、これらについて詳しく知りたい方は API の開発 を参照していただければと思います。
API の開発 では解説していない点を少し補足しておくと、まず、CommentAPI
と CommentDetailAPI
に関しては、先ほど定義した LoginRequiredAPIMixin
を継承してセッション認証を行うようにしている点がポイントとなります。API は公開すると誰でも実行できるようになってしまうので、必要に応じて認証を行い、API 利用者を制限することが重要となります。
そして、セッション認証を成功させるためには事前のログイン操作が必要となり、それを API で実施するためのメソッドが LoginAPI
の post
メソッドとなります。実施している処理を見ていただければ分かる通り、受け取る HTTP リクエストや返却する HTTP レスポンスのボディの形式の差異による処理の違いはあるものの、基本的には通常のウェブアプリでログインを実現するビューと同様の処理内容となっています。この点からも、通常のウェブアプリのビューと API のビューとではボディのフォーマットの差異による処理の違いはあるものの、それ以外の部分は全く同じ処理によって実現することが可能であることを理解していただけるのではないかと思います。
また、コメントの更新 API とコメントの削除 API を CommentDetailAPI
に実装しているという点もポイントになります。基本的には、同じ URL を割り当てた API が提供する機能は、同じクラス(View
を継承しているクラス)のメソッドに実装することになります。下記の3つに関しては、URL が同じであるため、これらはすべて CommentDetailAPI
に実装しています。
CommentDetailAPI
:get
:コメントの詳細取得 APIpatch
:コメントの更新 APIdelete
:コメントの削除 API
また、View
を継承しているクラスでは、受け取った HTTP リクエストの「メソッドの名称を全て小文字に変換した名前」のメソッドが実行されるようになっているため、上記のように CommentDetailAPI
にメソッドを定義しておけば、HTTP リクエストのメソッドに応じた CommentDetailAPI
のメソッドが実行され、それにより適切な API が実行されることになります。
ビューと URL とのマッピング
最後に、先ほど views.py
に定義した各種クラスが HTTP リクエストを受け取った時に動作するよう urls.py
でビューと URL とのマッピングを行えば、API が完成することになります。
今回は、各 API の URL を下記のように定義しており、
- ログイン API:
/api/login/
- ログアウト API:
/api/logout/
- コメントの投稿 API:
/api/comments/
- コメントの一覧取得 API:
/api/comments/
- コメントの詳細取得 API:
/api/comments/{id}/
- コメントの更新 API:
/api/comments/{id}/
- コメントの削除 API:
/api/comments/{id}/
さらに、下記のように各種クラスのメソッドに API を実装しています。
LoginAPI
:post
:ログイン API
LogoutAPI
:post
:ログアウト API
CommentAPI
:get
:コメントの一覧取得 APIpost
:コメントの投稿 API
CommentDetailAPI
:get
:コメントの詳細取得 APIpatch
:コメントの更新 APIdelete
:コメントの削除 API
なので、各 URL の HTTP リクエストを受け取った時には、下記のクラスを動作させればよいことになります。
/api/login/
:LoginAPI
/api/logout/
:LogoutAPI
/api/comments/
:CommentAPI
/api/comments/{id}/
:CommentDetailAPI
つまり、上記の:の前側に記載した URL を受け取った時に、:の後ろ側のクラスが動作するように設定してやれば良いので、api/urls.py
は下記のように作成してやれば良いことになります(api/urls.py
は自身で作成する必要があります)。
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.LoginAPI.as_view()),
path('logout/', views.LogoutAPI.as_view()),
path('comments/', views.CommentAPI.as_view()),
path('comments/<int:id>/', views.CommentDetailAPI.as_view()),
]
test_project/urls.py
で、URL が /api/
から始まる際に api/urls.py
を参照するように設定しているため、上記のように api/urls.py
を作成することで、URL が /api/ + pathの第1引数
である HTTP リクエストをウェブアプリが受け付けるようになり、その URL に対応する API の各種クラスのメソッドが実行されるようになります。
そして、これにより、仕様で定義した API をユーザーが利用することができるようになります。
スポンサーリンク
動作確認
最後に動作確認を行っていきましょう!
動作確認用スクリプト
API の動作確認を行うためには、各種 API を実行するためのツールやスクリプトが必要となります。
今回は、動作確認用のスクリプトとして下記を利用していきたいと思います。このスクリプトをコピペし、api_client.py
というファイル名で保存しておいてください。
import requests
import json
scheme = 'http://'
hostname = 'localhost:8000'
session = requests.Session()
def call_get_comment_list_api(min_id=None, max_id=None):
# コメントの一覧取得APIのURLを作成
url = scheme + hostname + '/api/comments/'
# クエリパラメーターをURLの末尾に結合
if min_id is not None and max_id is not None:
url += '?min_id=' + str(min_id) + '&max_id=' + str(max_id)
elif min_id is not None:
url += '?min_id=' + str(min_id)
elif max_id is not None:
url += '?max_id=' + str(max_id)
# コメントの一覧取得APIを実行
response = session.get(url=url)
return response
def call_get_comment_detail_api(id):
# コメントの詳細取得APIのURLを作成
url = scheme + hostname + '/api/comments/' + str(id) + '/'
# コメントの詳細取得APIを実行
response = session.get(url=url)
return response
def call_post_comment_api(text):
# コメントの投稿APIのURLを作成
url = scheme + hostname + '/api/comments/'
# リクエストのボディを作成
data = json.dumps({
'text': text,
})
# リクエストのヘッダーを作成
headers = {
'Content-Type': 'application/json'
}
# コメントの投稿APIを実行
response = session.post(
url=url,
headers=headers,
data=data
)
return response
def call_patch_comment_api(id, text):
# コメントの更新APIのURLを作成
url = scheme + hostname + '/api/comments/' + str(id) + '/'
# リクエストのボディを作成
data = json.dumps({
'text': text,
})
# リクエストのヘッダーを作成
headers = {
'Content-Type': 'application/json'
}
# コメントの投稿APIを実行
response = session.patch(
url=url,
headers=headers,
data=data
)
return response
def call_delete_comment_api(id):
# コメントの削除APIのURLを作成
url = scheme + hostname + '/api/comments/' + str(id) + '/'
# コメントの投稿APIを実行
response = session.delete(
url=url
)
return response
def call_login_api(username, password):
# ログインAPIのURLを作成
url = scheme + hostname + '/api/login/'
# リクエストのボディを作成
data = json.dumps({
'username': username,
'password': password,
})
# リクエストのヘッダーを作成
headers = {
'Content-Type': 'application/json'
}
# ログインAPIを実行(sessionにsessionidが記録される)
response = session.post(
url=url,
headers=headers,
data=data
)
return response
def call_logout_api():
# ログアウトAPIのURLを作成
url = scheme + hostname + '/api/logout/'
# ログアウトAPIを実行
response = session.post(
url=url,
)
return response
def main():
# ログイン実施
username = input('username:')
password = input('password:')
response = call_login_api(username, password)
print(response.json())
if response.status_code != 200:
return
# コメントを3個投稿
for i in range(3):
response = call_post_comment_api('Hello World ' + str(i))
print(response.json())
if response.status_code != 201:
return
# コメントの一覧を取得
response = call_get_comment_list_api()
print(response.json())
if response.status_code != 200:
return
comments = response.json()
# コメントの詳細を取得
id = comments[-2]['id']
response = call_get_comment_detail_api(id)
print(response.json())
if response.status_code != 200:
return
# コメントの更新
id = comments[-2]['id']
response = call_patch_comment_api(id, 'Good bye')
print(response.json())
if response.status_code != 200:
return
# コメントの削除
id = comments[1]['id']
response = call_delete_comment_api(id)
if response.status_code != 204:
print(response.json())
return
# コメントが削除されたことの確認
response = call_get_comment_detail_api(id)
print(response.json())
if response.status_code != 404:
return
# ログアウトの実施
call_logout_api()
# ログアウトのAPIの実行に失敗することの確認
response = call_get_comment_list_api()
if response.status_code == 401:
print(response.json())
else:
print('意図しないエラーが返却されています')
main()
このスクリプトでは、main
関数から call_xxx_api
関数を呼び出し、call_xxx_api
から各種 API を実行するようになっています。main
関数からは、今回開発した API が全て実行されるようになっています。
今回、コメントの操作を行う API ではセッション認証が必要となりますが、requests.Session
のインスタンスから API を実行させるようにすれば(HTTP リクエストを送信させるようにすれば)、ログインした後であれば自動的にセッション ID を含むクッキーが送信されるようになります。なので、requests.Session
のインスタンスから API を実行するという点を除けば、セッション認証に関して特別な処理を行う必要はありません。
また、HTTP リクエストの送信の仕方等は下記ページで解説していますので、詳しくは下記ページを参照していただければと思います。ただし、今回は API での CSRF 対策を無効化していますので、動作確認用のスクリプトにおいても、CSRF に関する処理は不要となります。
【Django入門18】ウェブアプリのHTTPリクエストでの操作動作確認の準備
では、ここから、先ほど開発した API の動作確認を実施していきます。
まずは、コンソールアプリを2つ起動し、一方のコンソールアプリで掲示板アプリを起動してください。掲示板アプリの test_project
フォルダ(上側の階層の test_project
)に移動し、下記コマンドを実行することで掲示板アプリが起動することになります。
% python manage.py runserver
ただし、初めて掲示板アプリを利用するという方は、データベースおよびテーブルを作成するため、上記のコマンドを実行する前に次の2つのコマンドを実行しておく必要があります。
% python manage.py makemigrations
% python manage.py migrate
また、今回はログイン API の動作確認も必要であるため、事前にユーザー登録を実施しておく必要があります。このユーザー登録の手順については、下記ページの 動作確認 の節で解説していますので、この節を参考にしてユーザー登録を行っておいてください。
【Django入門10】ログイン機能の実現ログイン API の実行
掲示板アプリが起動したら(必要に応じてマイグレーションとユーザー登録も必要)、次は、もう一方のコンソールアプリで api_client.py
を保存したフォルダに移動し、下記コマンドを実行してください。
% python api_client.py
このコマンドを実行すると、まずログインを行うためのユーザー名とパスワードが入力されるようになっています。ここには、掲示板アプリで登録したユーザーのユーザー名とパスワードを入力してください。
username:username password:password
これらを入力するとログイン API が実行されることになります。
ログイン API でのログインに失敗した場合は、下記のように受信した HTTP レスポンスのボディが出力されます。この場合はユーザー名とパスワードの見直しを行ってください(パスワード等を忘れた場合は、ウェブブラウザから新たにユーザーを登録し、その登録したユーザーでログインするのが早いと思います)。
{'detail': 'ログインに失敗しました'}
コメントの投稿 API の実行
ログインに成功した場合は、続いて自動的にコメントの投稿 API が 3
回分実行され、先ほどと同様に HTTP レスポンスのボディが出力されることになります。投稿に成功した場合は、下記のように、投稿したコメントの各種フィールドの値が出力されます。
{'id': 1, 'user': 'YamadaHanako', 'text': 'Hello World 0', 'date': '2024-12-16T23:29:19.039Z'} {'id': 2, 'user': 'YamadaHanako', 'text': 'Hello World 1', 'date': '2024-12-16T23:29:19.067Z'} {'id': 3, 'user': 'YamadaHanako', 'text': 'Hello World 2', 'date': '2024-12-16T23:29:19.084Z'}
コメントの一覧取得 API の実行
コメントの投稿に成功した場合、次はコメントの一覧取得 API が実行されることになり、API から返却された HTTP レスポンスのボディが出力されることになります。この結果より、投稿済みのコメントの一覧が取得できていることが確認できます。
[{'id': 1, 'user': 'YamadaHanako', 'text': 'Hello World 0', 'date': '2024-12-16T23:29:19.039Z'}, {'id': 2, 'user': 'YamadaHanako', 'text': 'Hello World 1', 'date': '2024-12-16T23:29:19.067Z'}{'id': 3, 'user': 'YamadaHanako', 'text': 'Hello World 2', 'date': '2024-12-16T23:29:19.084Z'}]
コメントの詳細取得 API の実行
続いて、コメントの詳細取得 API が実行されることになります。URL の {id}
部分には、コメントの末尾から2番目のレコードの ID が指定されるようになっています。API 実行後には、API から返却された HTTP レスポンスのボディが出力され、投稿済みのコメントが取得できていることが確認できます。
{'id': 2, 'user': 'YamadaHanako', 'text': 'Hello World 1', 'date': '2024-12-16T23:29:19.067Z'}
コメントの更新 API の実行
さらに、コメントの詳細取得 API の実行後には、コメントの更新 API が実行されることになります。URL の {id}
部分には、コメントの末尾から2番目のレコードの ID が指定されるようになっています。さらに、コメントの本文を Good bye
に変更するように API が実行されることになります。API 実行後には、API から返却された HTTP レスポンスのボディが出力され、コメントの本文部分が Good bye
に更新されていることが確認できます。
{'id': 2, 'user': 'YamadaHanako', 'text': 'Good bye', 'date': '2024-12-16T23:29:19.067Z'}
コメントの削除 API の実行
コメントの更新 API の実行後には、コメントの削除 API が実行されることになります。URL の {id}
部分には、コメントの末尾のレコードの ID が指定されるようになっています。
さらに、コメントの削除 API 実行後には、コメントの削除 API によって削除されたコメントに対してコメントの詳細取得 API が実行されることになります。API から返却された HTTP レスポンスのボディは下記のように出力され、これより、直前に実行したコメントの削除 API によってコメントの削除に成功していることが確認できます。
{'detail': 'レコードが存在しません'}
ログアウト API の実行
その後には、ログアウト API が実行されることになり、さらにログアウト API の実行後に、コメントの一覧取得 API が実行されることになります。ただし、コメントの一覧取得 API から返却される HTTP レスポンスのボディは下記のように出力されることになります。これは、ログアウト API によってログアウトが実施され、それ以降はセッション認証に失敗して実行できなくなったからになります。
{'detail': '認証に失敗しました'}
ここまでの動作確認により、API からコメントの操作が行えることや、セッション認証に成功しないと API が実行できないこと、さらにログイン API を事前に実行しておくことでセッション認証に成功するようになること等が確認できたことになります。
今回は、基本的に各種 API の実行に成功する例を示しましたが、送信するボディの各種フィールドを不正な値に変更してやれば API でエラーが発生するようになり、そのエラーの内容に応じた HTTP レスポンスが得られることも確認できるはずです。
以上で、動作確認は終了です!
まとめ
このページでは、Django での Web API の開発の仕方について解説しました!
Web API を開発して公開することで、他のウェブアプリとの連携を実現し、それによって新たな価値を創出したり、利用者数や知名度アップも図ることが可能となります。
Web API を開発するためには、しっかり API の仕様を定義しておくことも重要となります。この仕様を定義してドキュメント化しておくことで、開発者は、その仕様を満たすように設計・実装を行えば良いだけになりますし、利用者が API の使い方を理解することもできるようになります。
また、今回は Django のみを利用して Web API を開発しましたが、次の Django 入門 の連載では、Django REST Framework を利用した Web API (REST API) の開発方法について解説していきます。今回は、Django のみを利用して Web API を開発したため、多少ビューのコードが複雑になってしまいましたが、 Django REST Framework を利用することで、もっと簡単に Web API を開発することができるようになります。
なので、今回のように Django のみを利用して Web API を開発する機会は少ないかもしれませんが、Web API がどういった処理によって実現されているのかを知っていることで、Django REST Framework を利用する場合でも API のカスタマイズ等をやりやすくなると思いますので、今回解説した内容は頭の片隅にでも置いておけば、必ずいつかは役に立つと思います!