13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

HanamiのアプリケーションをREST APIとして実装する

Last updated at Posted at 2020-01-19

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の設定は起動時に読み込まれないので、削除してもしなくても大丈夫です。

apps/web/application.rb
      # 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']
apps/web/application.rb
      # The relative path to templates
      #
-      templates 'templates'
+     # templates 'templates'

また、今回はメーラーも使用しないのでこのタイミングで削除します。

bookshelf/config/environment.rb
-  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 を作成してテストを書いていきます。

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.appapp に格納する必要があります

  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以下のルーティングを下記のように設定します

web/config/route.rb
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 も同様です。

apps/web/controllers/books/create.rb
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 の処理と同じですが、ガッツリとバリデーションも記述できるので、アクション側からそのあたりの責務を一通り引き取ることができます。

lib/bookshelf/validators/form/books_validator.rb
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にまとめたものです。こちらも保存処理関連の責務をアクションから剥がすことができます。

lib/bookshelf/interactors/books_interactor/create.rb
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 になりがちな点です。まだプロダクトレベルのサンプルも少なく手探りなところもありますが、自分の中で消化できたらそのあたりの記事も書けたらと思います。

13
6
0

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
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?