Hanamiは複数のアプリケーションをモノリシックに構築することができます。
複数のアプリケーションを実装するにあたり、REST APIとして実装するアプリケーションを作成する機会がありましたが、3年以上の前の Build a Web API with Hanami ぐらいしか情報がなく苦労したので、備忘録として残しておきます。
API化するHanamiのアプリケーションの準備
今回は公式のチュートリアルが終わった段階の web
アプリケーションに対してPOSTメソッド利用できるAPI化を施そうと思います。
APIに不要なView関連ディリクトリの削除
assets, views, template の各ディリクトリは不要なので削除しちゃってください。
web
- ├── assets
- │ ├── images
- │ ├── javascripts
- │ └── stylesheets
├── config
├── controllers
- ├── templates
- └── views
このままでは、Hanamiのサーバが起動するときに読み込みエラーになるので、 apps/web/application.rb
の各クラスの読み込み箇所を修正します。
※assetsの設定は起動時に読み込まれないので、削除してもしなくても大丈夫です。
# Relative load paths where this application will recursively load the
# code.
#
# When you add new directories, remember to add them here.
#
- load_paths << [
- 'controllers',
- 'views' #削除
+ load_paths << ['controllers']
# The relative path to templates
#
- templates 'templates'
+ # templates 'templates'
また、今回はメーラーも使用しないのでこのタイミングで削除します。
- mailer do
- root 'lib/bookshelf/mailers'
-
- # See https://guides.hanamirb.org/mailers/delivery
- delivery :test
- end
+ # mailer do
+ # root 'lib/bookshelf/mailers'
+ #
+ # # See https://guides.hanamirb.org/mailers/delivery
+ # delivery :test
+ # end
テストの作成
HanamiはTDDを推奨の設計手法としてしているのでテストを先に作成しましょう。
今回はPOSTのAPIを実装するので spec/web/controllers/books/create_spec.rb
を作成してテストを書いていきます。
RSpec.describe Web::Controllers::Books::Create, type: :action do
include Rack::Test::Methods
let(:app) { Hanami.app }
describe "create books" do
let(:request_body) do
{
bools: {
title: 'hogehoge',
author: 'foo'
}
}
end
let(:do_request) { post '/api/books', params: request_body, header: { 'Content-Type' => 'application/json' } }
it 'is http status 201' do
do_request
expect(last_response.status).to eq 201
expect(last_response.body).to eq request_body
end
it 'is expect response body' do
do_request
expect(last_response.body).to eq request_body
end
it 'is created created books' do
expect { do_request }.to change {
BookRepository.new.first.nil?
}.from(true).to(false)
end
end
end
アクションのテストとの違いは直接エンドポイントを叩いているところです。
エンドポイントを叩くためには下記のモジュールをincludeして Hanami.app
を app
に格納する必要があります
include Rack::Test::Methods
let(:app) { Hanami.app }
ルーティング
今回は /api/books
というエンドポイントを作成します。
まず config/environment.rb
において、下記のように設定することでホスト名の次に来るパスに api
を自動で追加できます。
mount Web::Application, at: '/api'
そして、Hanami には rails と同じような RESTful Resource(s) な記法で エンドポイントをルーティングできるのでapi以下のルーティングを下記のように設定します
resources 'books', only: [:create]
これでルーティングの設定は完了です。
今回は :create
なのでhttpメソッドは POST になりますが、 :update
の場合、httpメソッドは PATCH になります。PUT は存在しないので注意してください。
おそらく厳密に :create と :update を分けたいために PUTは廃止されたのかと思われます。
また、Hanami側から指定してくれているので PUT か PATCh で惑わずに済んで助かります。
Action(Controller)
エラー周りに改善の余地がありますが、バリデーションと保存処理はアクションから独立させています。
最終的に self.body
に入れた値がレスポンスボディとして返されるので、作成したリソースからJSON化したものをここに格納すればJSONのレスポンスボディとして返されます。 self.status
も同様です。
module Web
module Controllers
module Books
include Api::Action
accept :json
def call(params)
validate_result = Form::BooksValidator.new(params).validate
raise StandardError unless validate_result.success?
BooksInteractor.new.create(attribute)
self.body = response_body
self.status = 201
rescue => exception
self.status = 400
@error = exception
end
def response_body
JSON.dump(BooksRepository.new.last.to_hash)
end
end
end
end
Hanamiのバリデーションは コンポーネント指向な dry-validation
を模して実装されています。
今回やっていることは rails で言うところの StrongParamater の処理と同じですが、ガッツリとバリデーションも記述できるので、アクション側からそのあたりの責務を一通り引き取ることができます。
require 'hanami/validations'
module Form
class BooksValidator
include Hanami::Validations
validations do
required(:books).schema do
required(:title) { filled? }
required(:author) { filled? }
end
end
end
end
保存処理をIteratorにまとめたものです。こちらも保存処理関連の責務をアクションから剥がすことができます。
require 'hanami/interactor'
module BooksInteractor
class Create
include Hanami::Interactor
def initialize(params)
@attributes = {
title: params[:books][:title],
author: params[:books][:author]
}
end
def call
BooksRepository.new.create(@attributes)
end
end
end
これらの実装により、Web
アプリケーションをPOSTメソッドを持ったAPIにすることができます。
終わりに
Hanami を触っていて思ったことは Entity と Repository の元となっている ROM(Ruby Object Mapper) の理解や Itarator の使い所をわかっていないと、どうしても rails 感が抜けずに劣化 rails になりがちな点です。まだプロダクトレベルのサンプルも少なく手探りなところもありますが、自分の中で消化できたらそのあたりの記事も書けたらと思います。