Should I use resource routing even for non-CRUD routes (Rails)? - ruby-on-rails

Is it considered best practice to use resourceful routes in Rails whenever possible, even if the CRUD verbs don't really match the actions being performed (details follow)?
In my Rails app, I'm implementing an OAuth login system using sorcery's external module. I closely followed their official tutorial, which defines the routes for the OAuth methods like this.
# config/routes.rb
post "oauth/callback" => "oauths#callback"
get "oauth/callback" => "oauths#callback" # for use with Github, Facebook
get "oauth/:provider" => "oauths#oauth", :as => :auth_at_provider
Basically, auth_at_provider is called when the user clicks the "Login via [Provider Name]" button, and the callback is called once they log in via the external provider.
I left the routes as-is, but a teammate reviewing it suggested we use resource routing, like this for example:
resources :oauth only: [:index, :create, :show]
I guess this is technically possible, but for me the singular routes defined in the tutorial are much more intuitive and self-explanatory. So my questions are:
Is it considered better (or common) to use resourceful routes even in cases like this? or
Are resourceful routes just a shorthand for cookie-cutter routes, and should only be used for straightforward controllers?

I wouldn't use the resource(s) helpers. The name tells you it's used for resources and oauth logic are not resources.
You could refactor the routes a little bit though
namespace :oauth do
match :callback, via: [:get, :post]
get ":provider", action: :oauth, as: :at_provider
end
This creates this routes:
oauth_callback GET|POST /oauth/callback(.:format) oauth#callback
oauth_at_provider GET /oauth/:provider(.:format) oauth#oauth
They are basically the same routes, DRYer and without misleading "resource" wording.
*Note the little change from "auth_at_provider" to "oauth_at_provider" introduced by the namespace

It is generally considered best practice to use resourceful routing when you're actually doing CRUD on a resource, ie:
resources :users # for creating, reading, updating, deleting users
If you'd have to create an entirely new resource and controller just for one create endpoint (for example), I don't see any harm in breaking the pattern and using non-resourceful routes, but I try to avoid doing so.
You should try to use resourceful routing with names that makes sense, to keep your routes consistent:
scope path: 'oauth' do
resource :callback, only: [:show, :update] # use show/update instead of callback method
resources :providers, only: [:show] # use show instead of auth_at_provider
end
So your routes would look like:
POST oauth/callback
GET oauth/callback
GET oauth/providers/:id

Related

Overriding routes parameters with NIL in Rails?

Is there a way to use resource routing instead of writing the routes one by one if my methods for which the default expects parameters don't use parameters?
For example, if I had a routes file like below, the expected path for the update method would be like this: /cats/:id (docs)
# routes.rb
Rails.application.routes.draw do
resources :cats, only: [:create, :update]
end
However, I don't require any params for my update method, meaning the path should be /cats.
I know there's a way to rename the params and not use :id, but I didn't find anything on disabling them. I tried adding param: nil to the end of the line but it didn't work.
As I wrote initially, I know this can be done if I write the routes one by one like below. My question is whether I can use resources to do it. Thank you!
# routes.rb
Rails.application.routes.draw do
post 'cats', to: 'cats#create'
put 'cats', to: 'cats#update'
end
This is exactly the use case for singular resources. Quote from the Rails Guides:
Sometimes, you have a resource that clients always look up without referencing an ID. For example, you would like /profile to always show the profile of the currently logged in user.
Change our routing to
resource :cats, only: [:create, :update]
And the following routes will be created:
cats PATCH /cats(.:format) cats#update
PUT /cats(.:format) cats#update
POST /cats(.:format) cats#create
As far as I know, there is not, resource is just a helper to create the standard verb-based CRUD routes, if you want custom routes you need to define your update route the way you did in your second example, of course, you can still use resource for your create route and just pass only: :create.

In RoR, do I have to specify every controller method as a route by hand?

What I'm wondering, in this case where I want to create a few web service type controllers, is if there is a way to "export" what method is allowed to be called from the controller. I'm still very new to RoR, and its routes feature, but in the end, is it expected that a fully functional RoR application just has hundreds of routes? Not every controller method I'm creating falls into a "resource" category.
Rails routes come in several varieties – RESTful routes are merely the ones that happen to provide native support for Rails resources. Remember that event resource routes can be modified to have member and collection routes:
# routes.rb
resources :products do
member do
get 'short' #=> products/:product_id/short/:id
post 'toggle' #=> products/:product_id/toggle/:id
end
collection do
get 'sold' #=> products/sold
end
end
You can also nest resource routes within other resource routes:
# routes.rb
resources :products do
resources :comments #=> RESTful routes patterned as products/:product_id/comments/:id/:action
resources :sales do
get 'recent', :on => :collection
end
end
Another handy feature is named routing. The following route is not resourceful:
# routes.rb
match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase # Creates route path akin to purchase_path(:id)
Namespaced routes can be very helpful for organization and readability:
# routes.rb
namespace :admin do
resources :products #=> RESTful routes patterned as admin/products/:product_id/:action
end
So, basically, there's a route for everything you want to do, whether it's RESTful/resourceful or not. But yes, you need to write a route for every action you want exposed to your app.
Using the routes.rb file, you can create routes or pattern matching for routes, as well as parameterized routes, and nested routes. You should read more about this here.
You can also give routes their own method name such as my_new_route_path. If you really wanted to, you could just hardcode routes into your HTML. Please don't do this.
Every controller action needs routes that map to it.

Rails: Routing to a different controller based on request format

I'm writing an app where I want all requests for HTML to be handled by the same controller action. I have some other routes that are JSON-specific. Here's what my routes look like:
Blog::Application.routes.draw do
constraints format: :json do
resources :posts
end
match "(*path)" => "web#index"
end
The problem is that constraints is being interpreted as "this route only works with the specified format" rather than "skip this route and try the next one if the request is not in the specified format."
In other words, navigating to /posts in the browser gives me a 406 Not Acceptable because the URL is constrained to the JSON format. Instead, I want it to fall through to web#index if the request is for HTML, and hit the resourceful route if the request is for JSON. How can this be achieved?
(Using Rails 3.2.9.)
I've thought about this, and come to some conclusions.
You might not want all routes to return the single page app HTML, because there are some paths that you'll ideally want to return an HTTP 404 status code from.
Your HTTP routes will be different from your JSON routes
You definitely should be routing different formats to different controllers
It might be advantageous to define all your HTML routes in your Rails router, so that you can use it to generate a javascript router. There is at least one gem that does this
Rails does not have this functionality and this gem https://github.com/svenfuchs/routing-filter doesn't really seem like the right tool. Here is my attempt: Rails routing-filter route all html requests to one action
Having to namespace your JSON API under a module Api to avoid routing collisions is no bad thing.
Don't have content visible to Google on your single page app, or you'll get banned for duplicate content.
I took a slightly different approach, which doesn't really answer the question, but I think it has a few advantages:
Here is my config/routes.rb
FooApp::Application.routes.draw do
# Route all resources to your frontend controller. If you put this
# in a namespace, it will expect to find another frontend controller
# defined in that namespace, particularly useful if, for instance,
# that namespace is called Admin and you want a separate single page
# app for an admin interface. Also, you set a convention on the names
# of your HTML controllers. Use Rails's action_missing method to
# delegate all possible actions on your frontend controllers.
def html_resources(name, options, &block)
resources(name, options.merge(:controller => "frontend"), &block)
end
# JSON API
namespace :api do
resources :things, :except => [:edit, :new]
end
# HTML
html_resources :things, :only => [:edit, :new, :index, :show] do
get :other_action
end
end
class FrontendController < ApplicationController
def action_missing(*args, &block)
render :index
end
end
First of all I think different controllers for the same action is not the "Rails way".
Probably you can solve your problem with low level custom request handler in the middleware.
You can use format in your constraints, e.g.
match '*path', constraints: { path: /foo.*/, format: 'html'}, to: 'home#index', via: [:get]

Nested singular resource, not showing up in rake routes

I created a search and replace controller, with just an index action. Since it's meant to go under one of my restful controllers created by a scaffold, i setup the following in the routes file:
resources :sites do
resource :search_and_replace, only: [:index]
end
However, it does not appear when I run rake routes. If I switch to resources, it does. But the method name is site_search_and_replace_index. The pluralization of resource doesn't feel right either, since this is not around multiple records in a table.
The index action doesn't exist in a singular resource. This makes sense if you think of the meaning of the action: index of what, there's only one resource? Use show instead:
resources :sites do
resource :search_and_replace, only: [:show]
end
Are you sure you want to have search and replace as a resource? There might be other options that are more useful: Adding more restful actions

How to redirect when ActionNotFound in Rails

I have a controller SubscriptionsController with only actions new and create. How would I redirect to new if say someone tries to visit GET /subscriptions which would normally trigger the index action?
config/routes.rb
resource :subscriptions, :only => [:new, :create]
Using rails3 you can do it from a route, something like:
match "/subscriptions", :to => redirect("/subscriptions/new")
Edit:
From the comments it was made clear you want to capture more than that, using a wild card you can make it more generic. You may need to combine this form with the previous to deal with the non-slash form (or try the below form without a slash, I havent tried that). Also make sure to put these "catch all" routes below your other ones since routes are matched from top to bottom.
match "/subscriptions/*other", :to => redirect("/subscriptions/new")

Resources