プログラミングノート

一からものを作ることが好きなエンジニアの開発ブログです。

Railsを使ったRESTfulなAPIの作り方

サーバーと連携するiPhoneアプリをそろそろ個人でも作ろうかなと思ったので、とりあえず開発したことのある方法をまとめてみました。今回はrails 2.3.8, ruby 1.8.7, nokogiri 1.4.3.1な環境で作っています。

簡単な仕様

タスクをCRUDできるだけの単純なAPIを作ります。
下記のメソッドを用意して、XMLとJSONのフォーマットに対応します。

  method URI params その他
検索 GET /api/search.format kw=検索ワード kwがない場合は全件返す
表示 GET /api/tasks/id.format  
登録 POST /api/tasks/id.format name=タスク  
編集 PUT /api/tasks/id.format name=タスク  
削除 DELETE /api/tasks/id   レスポンスヘッダのみ返す

開発

まずはプロジェクトと必要なファイルを作ります。

$ rails api -d mysql
$ cd api
$ script/generate controller api/tasks
$ script/generate model task name:string
$ rake db:create
$ rake db:migrate
$ script/console
Loading development environment (Rails 2.3.8)
ruby-1.8.7-p302 > Task
 => Task(id: integer, name: string, created_at: datetime, updated_at: datetime)

次に仕様に沿ってルーティングを設定します。(/config/routes.rb)
.:formatはxml, jsonなど複数のフォーマットに対応するための記述です。

ActionController::Routing::Routes.draw do |map|
...
  map.namespace :api do |api|
    api.connect 'search.:format',
      :controller => :tasks,
      :action => :search,
      :conditions => { :method => :get }

    api.connect 'tasks/:id.:format',
      :controller => :tasks,
      :action => :show,
      :id => /\d+/,
      :conditions => { :method => :get }

    api.connect 'tasks.:format',
      :controller => :tasks,
      :action => :create,
      :conditions => { :method => :post }

    api.connect 'tasks/:id.:format',
      :controller => :tasks,
      :action => :update,
      :id => /\d+/,
      :conditions => { :method => :put }

    api.connect 'tasks/:id',
      :controller => :tasks,
      :action => :delete,
      :conditions => { :method => :delete }
...
end

正しく設定できているかは下記のコマンドで確認できます。

$ rake routes
(in /path/to/project)
 GET    /api/search(.:format)          {:action=>"search", :controller=>"api/tasks"}
 GET    /api/tasks/:id(.:format)       {:action=>"show", :controller=>"api/tasks"}
 POST   /api/tasks.format              {:action=>"create", :controller=>"api/tasks"}
 PUT    /api/tasks/:id(.:format)       {:action=>"update", :controller=>"api/tasks"}
 DELETE /api/tasks/:id                 {:action=>"delete", :controller=>"api/tasks"}

続いてAPIの実装です。
そんなに多くないので全部一気に。

class Api::TasksController < ApplicationController
  require 'nokogiri'
  skip_before_filter :verify_authenticity_token # allow CSRF

  # GET /search.:format
  def search
    tasks = Task.find(:all, :conditions => ["name like ?", "%#{params[:kw]}%"])
    respond_to do |format|
      format.xml { render :xml => create_xml(tasks) }
      format.json { render :json => create_json(tasks) }
    end
  end

  # GET /tasks/id.:format
  def show
    task = Task.find_by_id(params[:id])
    (render_error(404, "resource not found") and return) unless task
    respond_to do |format|
      format.xml { render :xml => create_xml(task) }
      format.json { render :json => create_json(task) }
    end
  end

  # POST /tasks/id.:format
  def create
    (render_error(400, "missing name param") unless params[:name]) and return
    task = Task.create({ :name => params[:name] })
    respond_to do |format|
      format.xml { render :xml => create_xml(task), :status => 201 }
      format.json { render :json => create_json(task), :status => 201 }
    end
  end

  # PUT /tasks/id.:format
  def update
    task = Task.find_by_id(params[:id])
    render_error(404, "resource not found") and return unless task
    task.name = params[:name]
    task.save!
    respond_to do |format|
      format.xml { render :xml => create_xml(task) }
      format.json { render :json => create_json(task) }
    end
  end

  # DELETE /tasks/id
  def delete
    task = Task.find_by_id(params[:id])
    render_error(404, "resource not found") and return unless task
    task.delete
    head 200
  end

  private

  def render_error(status, msg)
    render :text => msg, :status => status
  end

  def create_xml(tasks)
    tasks = [tasks] unless tasks.class == Array
    xml = build_xml do |xml|
      xml.tasks {
        tasks.each do |task|
          xml.task(:id => task.id) {
            xml.name(task.name)
            xml.created_at(task.created_at)
          }
        end
      }
    end
  end

  def create_json(tasks)
    tasks = [tasks] unless tasks.class == Array
    tasks = tasks.inject([]){ |arr, task|
      arr << {
        :id => task.id,
        :name => task.name,
        :created_at => task.created_at
      }; arr
    }
    { :tasks => tasks }.to_json
  end

  def build_xml(&block)
    builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') { |xml| yield(xml) }
    builder.to_xml
  end
end

プロジェクトを作成するとデフォルトでCSRF対策のためのコードが入っているのですが、このままだとPOSTリクエストでデータの登録ができないので、before_filterで無効にしています。

XMLの整形については、ActiveRecordのto_xmlそのままでは使いにくいので、Nokogiriを利用しています。

動作確認

最後に、script/serverでAPIサーバーを起動して、正しく接続できるか確認します。ここではcurlを利用していますが、FirefoxだとRESTTestというアドオンが便利です。

登録 [POST] /api/tasks/id.format

$ curl -X POST -d "name=task1" http://localhost:3000/api/tasks.xml

<?xml version="1.0" encoding="UTF-8"?>
<tasks>
  <task id="1">
    <name>task1</name>
    <created_at>2010-09-11 07:13:26 UTC</created_at>
  </task>
</tasks>
$ curl -X POST -d "name=task1" http://localhost:3000/api/tasks.json

{"tasks":[{"created_at":"2010-09-11T07:14:17Z","name":"task1","id":2}]}

こんな感じでデータが返ってきます。
その他のメソッドも以下のコマンドで確認できます。

編集 [PUT] /api/tasks/id.format

$ curl -X PUT -d "name=task1 modified" http://localhost:3000/api/tasks/1.xml
$ curl -X PUT -d "name=task1 modified" http://localhost:3000/api/tasks/1.json

取得 [GET] /api/tasks/id.format

$ curl -X GET http://localhost:3000/api/tasks/1.xml
$ curl -X GET http://localhost:3000/api/tasks/1.json

検索 [GET] /api/search.format

$ curl -X GET -d "kw=task" http://localhost:3000/api/search.xml
$ curl -X GET -d "kw=task" http://localhost:3000/api/search.json

削除 [DELETE] /api/tasks/id

$ curl -X DELETE http://localhost:3000/api/tasks/1

完成!

細かいエラー処理が入ってなかったり、パラメータの辺りが微妙だったりしますが、そこら辺を調整すれば立派なAPIの完成です。

実際のサービスで使えるようにしようとすると、リソース単位で奇麗に分割できなかったりするので設計が結構難しいのですが、スマートフォンアプリなどから利用できるAPIを考えた場合、対象となるリソースが少なく、設計もそこまで複雑にはならないため、十分使えるかなーと思います。