28
  1. List item

My config/routes.rb file...

Rails.application.routes.draw do

  namespace :api, defaults: {format: 'json'} do
    namespace :v1 do
      resources :hotels do
        resources :rooms
      end
    end
  end

My app/controllers/api/v1/hotels_controller.rb

module Api
    module V1
        class HotelsController < ApplicationController
            respond_to :json
            skip_before_filter :verify_authenticity_token

            def index
                @hotels = Hotel.all
                respond_with ({hotels: @hotels}.as_json)
                #respond_with(@hotels)
            end

            def show
                @hotel = Hotel.find(params[:id])
                respond_with (@hotel)
            end

            def create
                @hotel = Hotel.new(user_params)
                if @hotel.save
                    respond_with (@hotel) #LINE 21
                end
            end

            private

                def user_params
                    params.require(:hotel).permit(:name, :rating)
                end
        end
    end
end

When I go to POST through Postman, my data saves just fine, but I get this NoMethodError. Why is this? The issue seems to be occurring at line 21, which is the respond_with(@hotel) line. Should it not just be responding with json ouput for the newly created hotel, via the show method?

(1.1ms)  COMMIT
Completed 500 Internal Server Error in 76ms

NoMethodError (undefined method `hotel_url' for #<Api::V1::HotelsController:0x0000010332df58>):
  app/controllers/api/v1/hotels_controller.rb:21:in `create'


  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_source.erb (1.0ms)
  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (1.7ms)
  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (1.4ms)
  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (31.5ms)
chris P
  • 6,359
  • 11
  • 40
  • 84

1 Answers1

58

Because your route is in the API + v1 namespace, you actually need to redirect to the api_v1_hotel_url(@hotel) after you successfully create your resource. Of course, this is an API and there is no real redirecting, but the default Rails responder doesn't know that. It also doesn't know about your routing namespaces.

With just the default responder, you would have to do

respond_with :api, :v1, @hotel

So that Rails will build a URL that exists. Alternatively, you can create a custom responder that remove the :location option. Here is the default responder: http://api.rubyonrails.org/files/actionpack/lib/action_controller/metal/responder_rb.html

Reading through the source code for that class is very helpful in understanding respond_with. For example, you don't need to use if record.save before you use respond_with with this Responder. Rails will check if the record saved successfully for you and render a 422 with errors if it failed to save.

Anyway, you can see that the responder sets up a lot of variables in it's initializer:

def initialize(controller, resources, options={})
  @controller = controller
  @request = @controller.request
  @format = @controller.formats.first
  @resource = resources.last
  @resources = resources
  @options = options
  @action = options.delete(:action)
  @default_response = options.delete(:default_response)
end

If you subclassed this responder, you could make something like this:

class CustomResponder < ActionController::Responder
  def initialize(*)
    super
    @options[:location] = nil
  end
end

You can set a controller's responder using responder=:

class AnyController < ActionController::Base
  self.responder = CustomResponder

  # ...
end

To be clear, let me recap:

  1. When you use respond_with, Rails will try to infer what route to redirect to after a successful create. Imagine you had a web UI where you can create hotels. After a hotel is created, you will be redirected to that hotel's show page in the standard Rails flow. That is what Rails is trying to do here.
  2. Rails does not understand your route namespaces when inferring the route, so it attempts hotel_url - a route which does not exist!
  3. Adding symbols in front of the resource will allow Rails to infer the route correctly, in this case api_v1_hotel_url
  4. In an API, you can make a custom responder which just sets the inferred location to nil, since you don't actually need to redirect anywhere with a simple JSON response. Custom responders can also be useful in many other ways. Check out the source code.
Logan Serman
  • 29,447
  • 27
  • 102
  • 141
  • @LoganSherman Hey thanks I ended up figuring this out yesterday, but then today I have a VERY similar issue I can't figure out. I did respond_with :api, :v1, AThotel to solve the hotel issue. But in my rooms_controller, I have the same issue. The route is defined in my config/routes files above. I tried respond_with :api, :v1, :hotel, ATroom and respond_with :api, :v1, ATroom and neither work. Am I doing that right. According to rake routes I want api_v1_hotel_room. How come this isn't working then? AT* = the at symbol. – chris P May 11 '14 at 02:32
  • If your rooms is nested under hotels, you probably have `@hotel` as well then yes? You should use `respond_with :api, :v1, @hotel, @room` so that Rails can inspect `@hotel`, determine it's ID, and generate the route (something like /api/v1/hotels/1/rooms/1). I really recommend overriding the location option for APIs, it really will clean up your controller. Remembering to add all of those specifications will be very annoying as your API grows. – Logan Serman May 11 '14 at 05:41
  • Thanks again. The location stuff is a bit over my head, I've only been working with Rails since April 1st, I didn't know a word of Ruby or a thing about Rails before that. Do you know any good videos/tutorials on what you're explaining, or any resources that may dumb it all down for me? :) – chris P May 11 '14 at 18:33
  • Logan Serman, could you take a look at this issue? http://stackoverflow.com/questions/34345695/no-method-error-rails-on-ajax-post-request. I'm having something similar, but based on this post I'm not sure what I should do. – Sean Magyar Dec 17 '15 at 23:53
  • Logan, problem solved, I misunterstood my problem. I thought it was beacuse of passing the url to js, but just simply forgot to set the even_path to be nested under user. – Sean Magyar Dec 18 '15 at 00:03
  • Is there something that can be done to stop rails trying to redirect? I get the error described in the OP, but the create action works before the error occurs (so the functionality of the API is unhindered).The error just looks ugly. Is there a way to `redirect_to :nowhere` or something? – stevec Jul 05 '19 at 18:18
  • Thank you for this. I used the 3rd option. In my `create` action, I simply added `api_v1_level_url` to the already existing `@level` location definition, that is `location: api_v1_level_url(@level)`. This was helpful. – Promise Preston Dec 09 '20 at 05:11