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を考えた場合、対象となるリソースが少なく、設計もそこまで複雑にはならないため、十分使えるかなーと思います。