JVG
JVG

Reputation: 21180

Rails: Where do I put my API methods?

I'm very new to Rails, and I'm a little overwhelmed where I do simple things like create an API call. I've set up a route at /reports which has this controller:

class ReportsController < ApplicationController

  @client = # Api-accessing gem

  @all_reports = []

  def self.request_report

    begin
      puts "Step 1:"
      step1 = @client.request_report(opts = {"max_count" => 1})
      step1_result = step1.parse
      puts "Done!"
      puts step1_result

    rescue Excon::Errors::ServiceUnavailable => e
      puts "Didn't work"
      logger.warn e.response.message
      retry
    end
  end # End request_report

  request_report

end

This correctly calls the external API when I first load the /reports route, but when I refresh the page the code isn't re-run.

Perhaps I'm misunderstanding what controllers are used for? Am I meant to be putting this code somewhere else? Or is there a caching issue?

Upvotes: 4

Views: 2727

Answers (3)

Richard Peck
Richard Peck

Reputation: 76784

To add to @max's excellent answer, you need to appreciate that Rails is based on a stateless protocol (HTTP)...

each request message can [only] be understood in isolation.

This means that if you want to create a set of controller actions, you have to appreciate that each call is going to create a new instance of your classes etc. This, coupled with the idea of a RESTful set of actions, should give you a basis from which to build your functionality.

--

#config/routes
scope constraints: { subdomain: "api" } do
  resources :reports #-> http://api.url.com/reports
end

#app/controllers/reports_controller.rb
class ReportsController < ApplicationController
   respond_to :json #-> requires "responders" gem

   def index #-> instance method
      @reports = Report.all
      respond_with @reports #-> all reports
   end

   def show
      @report = Report.find params[:id]
      respond_with @report
   end
end

I'll leave the service object stuff as I have no experience with it.

--

If you're pulling from an external API, you have several considerations:

  1. Calls ideally need to be asynchronous (unless you use multi-threading)
  2. Calls need to be made in the instance method

Your current pattern calls the API on the class, which is why you can't refresh it:

class ReportsController < ApplicationController
  @client = # Api-accessing gem

@client is only invoked (I don't know why it works, as it should be a class variable) with the class.

So if you send a new request (which creates an instance of ReportsController), @client is going to be declared that one time.

To get it working correctly, @client needs to be defined with each instance method:

class ReportsController < ApplicationController
  def index
    @client = # Api-accessing gem

This way, each time you invoke ReportsController#index, a new API call will be made. Might seem trivial, but the data scope is massive.


Finally, you need to read up about MVC (Model View Controller):

enter image description here

This will show you how controllers are meant to be used in Rails applications etc.

Upvotes: 2

max
max

Reputation: 102443

The only public API of controller are the actions which respond to a HTTP request. In your case get "/reports" => "reports#request_report" is a route which corresponds to the action request_report.

However actions are instance methods, not class methods:

class ReportsController
  def request_report # self.request_report would make this a class method!
    # @todo get reports from somewhere and
    # return some sort of response.
  end

  # any method call here happens when the class is evaluated.
end

You are declaring the action as a class method and then calling it when the ReportsController class is evaluated. Sorry to say but just about everything about your controller is wrong.

The Rails convention would be to call the action index.

Controllers in Rails should only be instantiated by the router (or your test framework). So they are definatly the wrong place to put resuable bits and bobs. If you ever see someone doing ReportsController.new.foo or ReportsController.foo - fire them on the spot.

So where do you put external API calls?

If its a pretty trivial one-off you can place it in private method in your controller.

Some place API calls on the model layer - however that is debatable since ActiveRecord models already are supercharged to the gills with powers and responsibilities.

One solution that has worked well for me is Service Objects. They are easy to test and have a clear single responsibility.

class RequestReportService
  def initalize(client)
    @client = client
  end 
  def call(opts = {})
   begin
      return @client.request_report(opts.merge("max_count" => 1))
    rescue Excon::Errors::ServiceUnavailable => e
      nil
    end
  end
end

class ReportsController
  def index
    @reports = RequestReportService.new(@client).call
  end
end

Upvotes: 8

uoroaix
uoroaix

Reputation: 97

Well I actually never seen anyone code like this in a rails controller. Rails is a mvp framework. Controller are use to negotiate between your model and the views. First of all, if you routed correctly to your controller like get "/reports" => "request_report#reports" your controller should have a method like the following def request_report @client = Client.find(params[:id]) end And then the controller will render and display the view in your app/views/reports/request_report.html.erb with access to the @client variable you just search from your database.

I am not sure why you are calling the block request_report at the bottom of the page, it just doesn't make sense in a controller. And you certainly don't really need to write self in front of a controller method. def self.request_report your code end As for where to put your api controller, usually for an api controller, we can create new folders under controllers, so the structure will be like app/controllers/api/v1/your_api_controller.rb Then in your_api_controller.rb you will need to add namespace infront of your controller like this. class Api::V1::ReportsController < ActionController::Base end It is the same with your routes, you will add namespace in your route.rb namespace :api do namespace :v1 do get "/reports" => "request_report#reports" end end

Upvotes: 0

Related Questions