Versioning Rails API - ruby-on-rails

I have searched around for a couple of days trying to figure out if I am appropriately versioning my rails api. I haven't quite found a good answer that helps me feel comfortable with my current approach. Thus, I have decided to take to stackoverflow to get my fellow Rails mates opinions. Lets look at some code:
I first started out by adding a namespace :api with a subdomain of 'api' to my api in routes.rb. I also added a scope for v1.
require 'api_constraints'
HostApi::Application.routes.draw do
# Api Definition
namespace :api, defaults: { format: :json }, constraints: { subdomain: 'api' }, path: '/' do
# Scoping Api Version
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
# resources here
end
end
end
I want to give those who hit the api to get back the current default version unless they specify a different version through headers. To handle this, I created an api_constraints in my lib directory.
lib/api_constraints.rb & lib/spec/api_constraints_spec.rb for testing.
class ApiConstraints
def initialize(options)
#version = options[:version]
#default = options[:default]
end
def matches?(request)
#default || request.headers["Accept"].include?("application/vnd.host_api.v#{#version}")
end
end
My test's pass and everything feels good. What I am curious about is when I start adding version 2,3,4; Can they be scoped the same way as v1 is scoped? Example:
require 'api_constraints'
HostApi::Application.routes.draw do
# Api Definition
namespace :api, defaults: { format: :json }, constraints: { subdomain: 'api' }, path: '/' do
# Scoping Api Version
scope module: :v1, constraints: ApiConstraints.new(version: 1) do
# resources here
end
scope module: :v2, constraints: ApiConstraints.new(version: 2, default: true) do
# resources here
end
end
end
I would imagine that changing passing default true to ApiConstraints in v2 scope would now set it as the default response when a version isn't requested through headers. Am I understanding this correctly? Would there be a better approach to handling the version than this? Thoughts, ideas, opinions greatly appreciated.
Side question. I did also change Rails.application.routes.draw in the routes.rb file, to HostApi::Application.routes.draw. This is commonly what I have seen others do, but I am unsure what the benefit to doing this is. If someone could help elaborate I would be very thankful. Thank you in advance for anyone who takes the time to help me understand this or simply share their thoughts.

I've used the same ApiConstraints in my rails-api-base project and it crashes when you try matches? with a non-default version without specifying the Accept header.
I added the following case to my tests (which crashes):
it 'returns false when not default and no Accept header' do
request = double(host: 'api.host_api.dev')
expect(api_constraints_v1.matches?(request)).to be false
end
And I fixed ApiConstraints:
def matches?(req)
#default ||
(req.respond_to?('headers') &&
req.headers.key?('Accept') &&
req.headers['Accept'].include?("application/vnd.host_api.v#{#version}"))
end
Hope it helps!

Related

How to accurately define Controller name in Rails?

Apologies for the basic question, but I'm trying to create an endpoint so I can a TranslationApi in my backend via my VueJs frontend via Fetch, so I need to make an endpoint I can insert. I'm attempting to create a route to make that happen, however when I run bin/rails routes | grep CcApiController I receive the following error:
ArgumentError: 'CcApiController' is not a supported controller name. This can lead to potential routing problems. See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use
I've read the documentation linked, but I'm not managing to fix this, can someone explain where I'm going wrong here? I'll link the files I've changed below:
cc_apis_controller.rb
module Panel
class CcApisController < CcenterBaseController
def index
run Ccenter::Adapters::Zendesk::TranslationApi.call(2116449)
end
end
end
panel_routes.rb
def draw_api_routes
resources: CcApiController
end
routes.rb
Rails.application.routes.draw do
resources :CcApiController, only: [:index]
end
API method I need to create Route for:
def make_request
response = Faraday.post('https://api.deepl.com/v2/translate', auth_key: '', text: #final_ticket, target_lang: 'DE', source_lang: 'EN')
if response.status == 200
body = response.body
message_element = body.split('"')[-2]
return message_element
else
raise InvalidResponseError unless response.success?
end
end
The answer to that is pretty simple.
Route names are snake_case, and match the controller's file name just omit the _controller suffix. In your case it is
Rails.application.routes.draw do
namespace :panel do
resources :cc_apis, only: %i[index]
end
end
For more info check this documentation and this article.

How to route to a specific custom Rails 5 API version?

I'm developing an application which backend is being written in rails 5 api (beta version).
My API will have some versions, and I'm using this approach to address versioning:
https://github.com/iamvery/rails-api-example/blob/master/config/routes.rb
Rails.application.routes.draw do
def api_version(version, &routes)
api_constraint = ApiConstraint.new(version: version)
scope(module: "v#{version}", constraints: api_constraint, &routes)
end
api_version(1) do
resources :articles, only: :index
end
api_version(2) do
resources :articles, only: :index
end
end
The thing is when I don't specify a version, it shows me an (obviuos) error (ActionController::RoutingError: No route matches [GET] \...).
But I'd like to route using latest api version instead throwing an error.
Your routes.rb file
Rails.application.routes.draw do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
# Then for a new version create a new scope
end
end
Create a new api_constraints.rb file in the app/lib directory
class ApiConstraints
def initialize(options)
#version = options[:version]
#default = options[:default]
end
def matches?(req)
#default || req.headers['Accept'].include?("application/vnd.marketplace.v#{#version}")
end
end
I would add a root route, and use a simple redirect, like this:
root to: redirect('/api/v2')
I believe this could be done dynamically, by a little more change, something like this:
#versions = []
def api_version(version)
#versions << versions
# The rest of your code..
end
root to: redirect("/v#{#versions.max}")
I hope this helps.

Rails Take all actions in the controllers in area

In my rails application I add an "api" area with controllers
In the route.rb file
I add the area
namespace :api do
#get "dashboard/sales_rate"
end
The controllers Class:
class Api::DashboardController < Api::ApplicationController
before_filter :authenticate_user!
def user_service
render :json => {"user_id" => current_user.id}
end
The Path is:
app/controllers/api/dashboard_controller
My question is if I can that the route take all action
for example /api/dashboard/user_service
and I will not add for each route row on the route.rb page
/api/{controller_under_api_namespace}/{action}
You can do with some meta programming sprinkle!
Api.constants.each |c|
c.action_methods.each do |action|
get [c.controller_name, action].join('/')
end
end
This method limits only to GET request. You can overcome it with RESTful routing.
Api.constants.each |c|
resources c.controller_name.to_sym
end
Hope, that helps. :)
I try add the code on the route.rb file
and I got this error
This is my file
But before trying to fix this part of code, I want to know if it's can change the performance or the calls to those pages?
If it not good for the performance I leave this option.

Better way to respond api version in returned JSON

I wonder know how to respond the api version in JSON
because I need to hardcode the version in the response know.
returned_data = {
status: "ok",
version: "version_num",
data: symbols
}
render json: Oj.dump(returned_data)
It seems not elegent, because I need to put the similar code in each function.
Any better practice in Rails
routes.rb
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1) do
resources :products
end
scope module: :v2, constraints: ApiConstraints.new(version: 2, default: true) do
resources :products
end
end
update (lx00st's suggestion)
before_action :response_template
private
def response_template
#respose = {
status: "ok",
version: "v1"
}
end
def EVERY_METHOD
#respose[:data] = symbols
render json: Oj.dump( #respose )
end
If you're creating an API you should consider using JSON builders rather than simply rendering JSON the way you are. One nice solution is ActiveModel::Serializers. This will allow you define a global serializer which always contains a certain bit of JSON, which will be merged into other responses. It also means you don't have to manually build up your JSON.

Rails 3.2 error routing issue. Error ID is conflicting with other object ID

We just upgraded our app to Rails 3.2.2 and are now having a routing issue for handling errors.
Per José Valim's blog post, we added the following:
config.exceptions_app = self.routes to config/application.rb
match "/404", :to => "errors#not_found" to config/routes.rb
(and the appropriate controller/views).
The problem is we need ourdomain.com/id to display an index page for a product category of id.
So, now ourdomain.com/404 shows our 404 page, when it should show our category listing page for the category with an id of 404.
How can we work around this?
Is there a way to make the app prepend each error with error_ before it's evaluated by routes?
Or, maybe somehow set config.exceptions_app to reference a namespace in the routes file?
Or, can I create a second route set and set config.exceptions_app = self.second_set_of_routes?
Thanks!
We had the same problem -- error codes colliding with ids for resources at the root level (e.g., collisions between ourdomain.com/:resource_id and ourdomain.com/404).
We modified José Valim's solution by adding a route constraint that only applies when handling an exception:
# Are we handling an exception?
class ExceptionAppConstraint
def self.matches?(request)
request.env["action_dispatch.exception"].present?
end
end
MyApp::Application.routes.draw do
# These routes are only considered when there is an exception
constraints(ExceptionAppConstraint) do
match "/404", :to => "errors#not_found"
match "/500", :to => "errors#internal_server_error"
# Any other status code
match '*a', :to => "errors#unknown"
end
...
# other routes, including 'match "/:resource_id"'
end
(We only stumbled on this solution last night, so it hasn't had much burn-in time. We are using Rails 3.2.8)
There's one solution which I've found so far:
# application_controller.rb
def rescue_404
rescue_action_in_public CustomNotFoundError.new
end
def rescue_action_in_public(exception)
case exception
when CustomNotFoundError, ::ActionController::UnknownAction then
#render_with_layout "shared/error404", 404, "standard"
render template: "shared/error404", layout: "standard", status: "404"
else
#message = exception
render template: "shared/error", layout: "standard", status: "500"
end
end
def local_request?
return false
end
rescue_action_in_public is the method that Rails calls to handle most errors.
local_request? the method tells Rails to stop sucking if it's local request
# config/routes.rb
match '*path', controller: 'application', action: 'rescue_404' \
unless ::ActionController::Base.consider_all_requests_local
It simply says that it can’t find any other route to handle the request (i.e. the *path) it should call the rescue_404 action on the application controller (the first method above).
EDIT
This version worked for me well!
Try to add to application.rb
# 404 catch all route
config.after_initialize do |app|
app.routes.append{ match '*a', to: 'application#render_not_found' } \
unless config.consider_all_requests_local
end
See: https://github.com/rails/rails/issues/671#issuecomment-1780159
It seems that this route is hard coded at the show_exceptions method (see source)
Sorry, but I don't think of a way of doing it besides changing the line 45 on the source above to:
env["PATH_INFO"] = "/error_#{status}"
(what is, needless to say, no solution at all).
It doesn't hurt to ask:If you thought it was nice to have your own error controller implemented so simply and desperately want to have it, than wouldn't it even be more "RESTful" if your route were yourdomain.com/product/:id?

Resources