| name | api-versioning |
| description | Implements RESTful API design with versioning and request tests. Use when building APIs, adding API endpoints, versioning APIs, or when user mentions REST, JSON API, or API design. Use when this capability is needed. |
| metadata | {"author":"dchuk"} |
API Versioning for Rails 8
Overview
Well-structured APIs need versioning for backwards compatibility and clear organization.
Recommended: URL Path versioning (/api/v1/users)
Quick Setup
Routes
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only: [:index, :show, :create, :update, :destroy]
resources :events, only: [:index, :show, :create]
end
end
end
Directory Structure
app/controllers/
├── api/
│ ├── base_controller.rb
│ ├── v1/
│ │ ├── base_controller.rb
│ │ ├── users_controller.rb
│ │ └── events_controller.rb
│ └── v2/
│ ├── base_controller.rb
│ └── users_controller.rb
Base Controller
module Api
class BaseController < ApplicationController
skip_before_action :verify_authenticity_token
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
def unprocessable_entity(exception)
render json: { errors: exception.record.errors }, status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
end
end
Version Base Controller
module Api
module V1
class BaseController < Api::BaseController
end
end
end
Resource Controller
module Api
module V1
class UsersController < BaseController
before_action :set_user, only: [:show, :update, :destroy]
def index
@users = User.page(params[:page]).per(25)
render json: { data: @users, meta: pagination_meta(@users) }
end
def show
render json: { data: @user }
end
def create
@user = User.create!(user_params)
render json: { data: @user }, status: :created
end
def update
@user.update!(user_params)
render json: { data: @user }
end
def destroy
@user.destroy
head
= .find(params[])
params.().permit(, )
()
{
collection.current_page,
collection.total_pages,
collection.total_count
}
API Authentication
Bearer Token Auth
module Api
class BaseController < ApplicationController
before_action :authenticate_api_user!
private
def authenticate_api_user!
token = request.headers["Authorization"]&.split(" ")&.last
@current_api_user = Session.find_by(token: token)&.user
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_api_user
end
def current_api_user
@current_api_user
end
end
end
Response Format
{ "data": { "id": 1, "name": "John", "email": "john@example.com" } }
{ "data": [...], "meta": { "current_page": 1, "total_pages": 10 } }
{ "error": "Record not found" }
{ "errors": { "email": ["has already been taken"] } }
Testing APIs (Minitest)
Request Test Template
require "test_helper"
class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:one)
@headers = {
"Accept" => "application/json",
"Content-Type" => "application/json",
"Authorization" => "Bearer #{api_token_for(@user)}"
}
end
test "GET /api/v1/users returns all users" do
get "/api/v1/users", headers: @headers
assert_response :success
data = json_response["data"]
assert_kind_of Array, data
end
test "GET /api/v1/users/:id returns the user" do
get "/api/v1/users/#{@user.id}", headers: @headers
assert_response :success
assert_equal @user.id, json_response["data"]["id"]
end
test "GET /api/v1/users/:id returns 404 for missing user" do
get "/api/v1/users/999999", headers: @headers
assert_response
assert json_response[].present?
test
params = { { , } }
assert_difference(, )
post , params.to_json,
assert_response
test
params = { { , } }
assert_no_difference()
post , params.to_json,
assert_response
assert json_response[].present?
test
params = { { } }
patch , params.to_json,
assert_response
assert_equal , .reload.name
test
assert_difference(, -)
delete ,
assert_response
test
get , { => }
assert_response
.parse(response.body)
()
user.sessions.create!.token
Checklist
Converted and distributed by TomeVault — claim your Tome and manage your conversions.