LoginSignup
13
12

More than 5 years have passed since last update.

PhoenixでREST-APIを実装する

Last updated at Posted at 2016-02-19

やること

これまで作成したデモアプリに、記事情報を取得するためのREST-APIを追加する

generatorを使う場合

コントローラの作成

mixコマンドを使ってAPI用のコントローラを作成する。

$ mix phoenix.gen.json ApiPost posts --no-model

記事を表すPostモデルはすでに存在するので、--no-modelオプションを付ける。
しかし、このままでは生成されたコントローラは存在しないApiPostというモデルを扱おうとしてしまうので、web/controllers/api_post_controller.exを開き、モデル名をApiPost->Postに置換する。

ルートの追加

web/routes.exを開き、以下の記述を追加

web/routes.ex
  scope "/api", PhoenixSample do
    pipe_through :api

    resources "/posts", ApiPostController
  end

実際にhttp://localhost:4000/api/postsにアクセスしてみると...

$ http -b http://localhost:4000/api/posts
{
    "data": [
        {
            "id": 1
        }, 
        {
            "id": 2
        }
    ]
}

ルートは有効になっているし、データも取れているようだが、IDしか表示されないのでは話にならない。

ビューの修正

ということで、ビューを修正する。
web/views/api_post_view.exを開き、内容を確認すると

web/views/api_post_view.ex
  def render("api_post.json", %{api_post: api_post}) do
    %{id: api_post.id}
  end

という記述に気がつく。
どうやら、generator使用時にmodelのプロパティを省略したため、IDしか表示されてなかったようだ。
そこで、以下のように表示項目を追加

web/views/api_post_view.ex
  def render("api_post.json", %{api_post: api_post}) do
    %{id: api_post.id,
      title: api_post.title, 
      content: api_post.content,
      inserted_at: api_post.inserted_at
     }
  end

改めてアクセスしてみると...

$ http -b http://localhost:4000/api/posts
{
    "data": [
        {
            "content": "希望の朝だ!", 
            "id": 1, 
            "inserted_at": "2016-02-08T03:26:39Z", 
            "title": "新しい朝が来た"
        }, 
        {
            "content": "輝く緑", 
            "id": 2, 
            "inserted_at": "2016-02-18T21:40:06Z", 
            "title": "新しい朝のもと"
        }
    ]
}

今度はID以外の情報もちゃんと取得できるようになりました。

しかし、今のままでは一覧情報と詳細情報とで同じ情報を取得するようになっている。
ブラウザ向けの画面にしろ、それ以外のクライアント向けのAPIにしろ、リストで取得する情報と
詳細情報で取得する情報は異なるものだと思うので、それに合わせて更にビューを修正することにする。
具体的には、一覧/詳細APIでは以下の様な情報を返すようにする。
- 一覧APIではIDとタイトルと投稿日時、それと詳細APIへのリンクを返す
- 詳細APIではIDとタイトルと内容、それとコメント一覧を返す
- ついでに、一覧APIで取得されるリストにつけられるキー名をdataからpostsにかえ、詳細APIで取得する情報については、ラップされていないオブジェクトとして返す

viewを変更した結果が以下のとおり

web/views/api_post_view.ex
defmodule PhoenixSample.ApiPostView do
  use PhoenixSample.Web, :view

  def render("index.json", %{posts: posts}) do
   %{posts: render_many(posts, PhoenixSample.ApiPostView, "api_post_header.json")}
  end

  def render("show.json", %{api_post: api_post}) do
    render_one(api_post, PhoenixSample.ApiPostView, "api_post_body.json")
  end

  def render("api_post_header.json", %{api_post: api_post}) do
    %{id: api_post.id,
      title: api_post.title, 
      inserted_at: api_post.inserted_at,
      link: api_post_path(PhoenixSample.Endpoint, :show, api_post)
     }
  end

  def render("api_post_body.json", %{api_post: api_post}) do
    %{id: api_post.id,
      title: api_post.title, 
      content: api_post.content,
      inserted_at: api_post.inserted_at,
      comments: render_many(api_post.comments, PhoenixSample.ApiPostView, "comment.json")
     }
  end

  def render("comment.json", %{api_post: comment}) do
    %{id: comment.id,
      name: comment.name,
      content: comment.content,
      inserted_at: comment.inserted_at
     }
  end
end

実際にアクセスしてみると...

$ http -b http://localhost:4000/api/posts
{
    "posts": [
        {
            "id": 1, 
            "inserted_at": "2016-02-08T03:26:39Z", 
            "link": "/api/posts/1", 
            "title": "新しい朝が来た"
        }, 
    {
            "id": 2, 
            "inserted_at": "2016-02-18T21:40:06Z", 
            "link": "/api/posts/2", 
            "title": "新しい朝のもと"
        }
    ]
}

$ http -b http://localhost:4000/api/posts/1
{
    "comments": [
        {
            "content": "喜びに胸を開け", 
            "id": 2, 
            "inserted_at": "2016-02-09T17:29:18Z", 
            "name": "名無し"
        }, 
        {
            "content": "大空あおげ", 
            "id": 3, 
            "inserted_at": "2016-02-09T00:00:00Z", 
            "name": "通りがかり"
        }, 
        {
            "content": "ラジオの声に", 
            "id": 6, 
            "inserted_at": "2016-02-09T10:33:14Z", 
            "name": "名無し2"
        }, 
         {
            "content": "健やかな胸を", 
            "id": 7, 
            "inserted_at": "2016-02-13T08:52:45Z", 
            "name": "3"
        }
    ], 
    "content": "希望の朝だ!", 
    "id": 1, 
    "inserted_at": "2016-02-08T03:26:39Z", 
    "title": "新しい朝が来た"
} 

これで、だいたい欲しかったものは出来上がりました。

ちなみに、詳細APIのURIを生成するために、Viewの中で呼び出している

      link: api_post_path(PhoenixSample.Endpoint, :show, api_post)

この関数ですが、ControllerやTemplateのなかでは

      api_post_path(conn, :show, api_post)

あるいは

      api_post_path(@conn, :show, api_post)

という形で使用されているので、同じようにconnを使った形で書きたかったのですが、controllerから渡されているはずの、このconnへのアクセスの仕方がわからなかったのでこのように書いています。
こちらの記事によると、引数のmapの中に含まれているようにも見えるのですが、もう少し詳しく調査してみないとわかりません。

(追記)Viewの中で、connにアクセスする方法について

コメントで @uasiさんに教えていただきました。情報ありがとうございます。
render関数の第二引数のキーとしてconnを指定すればアクセス可能とのことなので、

  def render("index.json", %{posts: posts}) do
   %{posts: render_many(posts, PhoenixSample.ApiPostView, "api_post_header.json")}
  end

  def render("index.json", %{posts: posts, conn: conn}) do
   %{posts: render_many(posts, PhoenixSample.ApiPostView, "api_post_header.json")}
  end

と変えてみたところ、connにアクセスすることができました。
しかし、このrender_manyを経由して呼び出される

  def render("api_post_header.json", %{api_post: api_post}) do
    %{id: api_post.id,
      title: api_post.title, 
      inserted_at: api_post.inserted_at,
      link: api_post_path(PhoenixSample.Endpoint, :show, api_post)
     }
  end

  def render("api_post_header.json", %{api_post: api_post, conn: conn}) do
    %{id: api_post.id,
      title: api_post.title, 
      inserted_at: api_post.inserted_at,
      link: api_post_path(conn, :show, api_post)
     }
  end

と変えてみると...
スクリーンショット 2016-02-21 13.27.06.png
とマッチする関数が見つからないというエラー。
なので、少し戻って、render_manyに渡している引数を、モデルのリストから
link先URIを含んだmapのリストに変えてみる。

  def render("index.json", %{posts: posts, conn: conn}) do
    posts = posts |> Enum.map(fn(post) -> Map.merge( Map.from_struct(post), %{link: api_post_path(conn, :show, post)}) end)

    %{posts: render_many(posts, PhoenixSample.ApiPostView, "api_post_header.json")}
  end

  def render("api_post_header.json", %{api_post: api_post}) do
    %{id: api_post.id,
      title: api_post.title, 
      inserted_at: api_post.inserted_at,
      link: api_post.link
     }
  end

このやり方なら、想定通りの出力が得られましたが、モデルを一度別のマップにいちいち変換しているのが、スマートじゃないような気がするんですよね…

追記2

@uasiさんから、さらなる情報。
たびたびありがとうございます。

render_many/4 の第4引数に conn を入れた Map をセットすると def render("api_post_header.json", %{api_post: api_post, conn: conn}) に渡されますよ:

実際に, render_manyの呼び出しを

    render_many(posts, PhoenixSample.ApiPostView, "api_post_header.json", %{conn: conn})

に変えてみたところ、

def render("api_post_header.json", %{api_post: api_post, conn: conn})

にマッチしました。
また、試しにrender_oneの第4引数に同じマップを渡してみたところ、こちらも同様の挙動が確認されました。
render_manyやrender_oneは第1引数(の各要素)を含むマップと第4引数のマップをマージしたものが、最終的にrender関数の第2引数として渡されるようです。

別のやり方

ブラウザ向けの出力とAPI向けの出力を同じコントローラで行う方法が、こちらの記事で紹介されておりました。
こちらも、非常にスマートなやり方だと思います。

ただ、個人的には、statelessに作りたいAPI機能と、セッション情報を使うであろうstatefulなブラウザ向け機能を一つのコントローラで処理しようとすると、お互いの事情の間に色々と齟齬が出て
面倒なことになるんじゃないかとも思います。

参考サイト

[Elixir] PhoenixでJSONを返すWeb APIを作る: http://qiita.com/FL4TLiN3/items/41ca80cdbfca1956ed78

Phoenix のコントローラで render/3 を呼んでからのレンダリング処理の流れを追う: http://qiita.com/uasi/items/a49d9c84d113dd54d0be

13
12
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12