爆速で軽量なRubyのREST APIを作る方法

フルスタックのRuby on Railsを使うのではなく、RESTfulなAPIをPythonのflaskのような手軽さで構築する方法を調べました。

# 下準備
今回はRuby on Railsで使われている一部のフレームワークを使います。

* Ruby 2.5
* Grape (RESTライクなAPI構築のためのフレームワーク)
* rack (HTTPリクエスト処理のためのフレームワーク)

Rubyはあらかじめインストールしておいてください。
Grapeとrackはgemを使ってインストールすることができます。

```
$ gem install grape
$ gem install rack
```

# APIの実装
Grapeを使って、APIのエンドポイントを記述します。

```ruby:hello.rb
require 'grape'

class HelloAPI < Grape::API
  format :json
  get '/' do
    # paramsでパラメータを取得
    { 'hello': 'world', 'params': params }
  end
end
```

rackの設定ファイルを以下のように記述します。

```ruby:config.ru
require './hello' # hello.rbが同じディレクトリに格納されていること
run Rack::Cascade.new [HelloAPI]
```

# 起動
`config.ru`の存在するディレクトリで、以下のコマンドを実行します。

```
$ rackup
```

http://localhost:9292
にアクセスして`{"hello":"world","params":{}}`が表示されることを確認。
http://localhost:9292?a=1
にアクセスすると`{"hello":"world","params":{"a":"1"}}`パラメータがそのまま返却されます。

非常に簡単に軽量なAPIを作成することができました。

# (参考) 自動再起動
rerunというツールを利用するするとソースコードの変更を自動的に検知して再起動してくれます。

```
$ gem install rerun
$ rerun rackup
```


# (参考) よく使う機能
## Request Body
Request Bodyは`env['api.request.body']`で取得できます。

```ruby
post '/' do
  body = env['api.request.body']
  return body # Response BodyにそのままRequest Bodyを返す
end
```
```
$ curl -X POST http://localhost:9292/ -H 'Content-Type:application/json' -d '{"OK": 0}'
{"OK":0}
```

## Pathパラメータの取得
`params`ハッシュから取得できます。

```ruby
get 'users/:user_id' do
  id = params[:user_id]
  return { user_id_param: id }
end
```

## Queryパラメータの取得
pathと同じく`param`ハッシュから取得できます。

```ruby
get '/' do
  name = params[:name]
  return { name_param: name }
end
```

```shell
$ curl -X get http://localhost:9292/?name=taro -H 'Content-Type:application/json'
{"name_param":"taro"}
```

## Headerの取得
`headers`ハッシュから取得できます。

```ruby
get '/' do
  id = headers['X-Account-Id']
  return { account_id: id }
end
```

```shell
$ curl -X GET http://localhost:9292/ -H 'X-Account-Id:123'
{"account_id":123}
```

## ステータスコードの指定

```ruby
put `/` do
  status 405
  return { message: 'not support' }
end
```

```shell
$ curl -v -X PUT http://localhost:9292/ -d 'xxx'
(省略)
< HTTP/1.1 405 Method Not Allowed
< Content-Type: application/json
< Content-Length: 25
(省略)
{"message":"not support"}
```

# (参考) 簡単な例
サンプルでユーザの管理APIを作りました。

## 呼び出し例
* 全てのユーザの取得

```shell
$ curl -X GET http://localhost:9292/users
[{"id":1,"name":"alice","age":20},{"id":2,"name":"bob","age":25}]
```

* ユーザのname検索

```shell
curl -X GET http://localhost:9292/users?name=bob
[{"id":2,"name":"bob","age":25}]
```

* あるユーザの取得

```shell
$ curl -X GET http://localhost:9292/users/1
{"id":1,"name":"alice","age":20}
```

* ユーザの作成

```shell
$ curl -X POST http://localhost:9292/users -H 'Content-Type:application/json' -d '{"name": "charlie", "age": 18 }'
{"name":"charlie","age":18,"id":3}
```

* ユーザの更新

```shell
curl -X PATCH http://localhost:9292/users/3 -H 'Content-Type:application/json' -d '{"age": 19 }'
{"name":"charlie","age":19,"id":3}
```

* ユーザの置換

```shell
curl -X PUT http://localhost:9292/users/3 -H 'Content-Type:application/json' -d '{"name": "dave", "age": 30 }'
{"name":"dave","age":30,"id":3}
```

* ユーザの削除

```shell
curl -X DELETE http://localhost:9292/users/1
```

## コード例
```ruby
require 'grape'

class Main < Grape::API
  format :json

  alice = { id: 1, name: 'alice', age: 20 }
  bob = { id: 2, name: 'bob', age: 25 }
  users = [alice, bob]

  get '/users' do
    name = ''
    name = params[:name] if params[:name].present?
    status 200
    return users.filter{|user| user[:name].include? name }
  end

  get '/users/:user_id' do
    id = params[:user_id]
    user = users.find{|user| user[:id] == id.to_i }
    if user.present? then
      status 200
      return user
    else
      status 404
      return { message: "Not Found User: #{id}" }      
    end
  end

  post '/users' do
    body = env['api.request.body'].symbolize_keys
    if body[:name].present? && body[:age].present? then
      body[:id] = users.map{|user| user[:id]}.max + 1
      users << body
      status 201
      return body
    else
      status 400
      return { message: "'name' and 'age' must be required" }
    end
  end

  patch '/users/:user_id' do
    body = env['api.request.body'].symbolize_keys
    id = params[:user_id]
    user = users.find{|user| user[:id] == id.to_i }

    if user.nil? then
      status 404
      return { message: "Not Found User: #{id}" } 
    end

    user[:name] = body[:name] if body[:name].present?
    user[:age] = body[:age] if body[:age].present?
    status 200
    return user
  end

  put '/users/:user_id' do
    id = params[:user_id]
    body = env['api.request.body'].symbolize_keys
    user = users.find{|user| user[:id] == id.to_i }

    if user.nil? then
      status 404
      return { message: "Not Found User: #{id}" } 
    end

    if body[:name].nil? || body[:age].nil? then
      status 400
      return { message: "'name' and 'age' must be required" }
    end

    user[:name] = body[:name]
    user[:age] = body[:age]
    status 200
    return user
  end

  delete '/users/:user_id' do
    id = params[:user_id]
    user = users.find{|user| user[:id] == id.to_i }

    if user.nil? then
      status 404
      return { message: "Not Found User: #{id}" } 
    end

    users.reject! {|user| user[:id] == id.to_i }

    status 204
    return ''
  end

end
```

# 参考文献
https://github.com/ruby-grape/grape
https://github.com/rack/rack