| 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. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
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