api-versioning

API Versioning for Rails

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "api-versioning" with this command: npx skills add thibautbaissac/rails_ai_agents/thibautbaissac-rails-ai-agents-api-versioning

API Versioning for Rails

Overview

Well-structured APIs need versioning for backwards compatibility and clear organization.

Versioning Strategies

Strategy URL Example Header Example

URL Path /api/v1/users

Query Param /api/users?version=1

Header /api/users

Accept: application/vnd.api+json; version=1

Accept Header /api/users

Accept: application/vnd.myapp.v1+json

Recommended: URL Path versioning (most common, easiest to understand)

Quick Setup

Routes

config/routes.rb

Rails.application.routes.draw do namespace :api do namespace :v1 do resources :users, only: [:index, :show, :create, :update, :destroy] resources :posts, only: [:index, :show, :create] end

# v2 with changes
namespace :v2 do
  resources :users, only: [:index, :show, :create, :update, :destroy]
end

end end

Directory Structure

app/controllers/ ├── api/ │ ├── base_controller.rb # Shared API logic │ ├── v1/ │ │ ├── base_controller.rb # V1 base │ │ ├── users_controller.rb │ │ └── posts_controller.rb │ └── v2/ │ ├── base_controller.rb # V2 base │ └── users_controller.rb

Base Controller

app/controllers/api/base_controller.rb

module Api class BaseController < ApplicationController # Skip CSRF for API requests skip_before_action :verify_authenticity_token

# Respond with JSON by default
respond_to :json

# Handle common errors
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

app/controllers/api/v1/base_controller.rb

module Api module V1 class BaseController < Api::BaseController # V1-specific configuration end end end

Resource Controller

app/controllers/api/v1/users_controller.rb

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 :no_content
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:name, :email)
  end

  def pagination_meta(collection)
    {
      current_page: collection.current_page,
      total_pages: collection.total_pages,
      total_count: collection.total_count
    }
  end
end

end end

Response Format

Standard JSON Response

{ "data": { "id": 1, "type": "user", "attributes": { "name": "John Doe", "email": "john@example.com", "created_at": "2024-01-15T10:30:00Z" } } }

Collection Response

{ "data": [ { "id": 1, "type": "user", "attributes": { ... } }, { "id": 2, "type": "user", "attributes": { ... } } ], "meta": { "current_page": 1, "total_pages": 10, "total_count": 100 } }

Error Response

{ "error": "Record not found", "code": "not_found" }

{ "errors": { "email": ["has already been taken"], "name": ["can't be blank"] } }

Testing APIs

Request Spec Template

spec/requests/api/v1/users_spec.rb

require 'rails_helper'

RSpec.describe 'Api::V1::Users', type: :request do let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } }

describe 'GET /api/v1/users' do let!(:users) { create_list(:user, 3) }

it 'returns all users' do
  get '/api/v1/users', headers: headers

  expect(response).to have_http_status(:ok)
  expect(json_response['data'].size).to eq(3)
end

it 'returns paginated results' do
  get '/api/v1/users', params: { page: 1 }, headers: headers

  expect(json_response['meta']).to include('current_page', 'total_pages')
end

end

describe 'GET /api/v1/users/:id' do let(:user) { create(:user) }

it 'returns the user' do
  get "/api/v1/users/#{user.id}", headers: headers

  expect(response).to have_http_status(:ok)
  expect(json_response['data']['id']).to eq(user.id)
end

context 'when user not found' do
  it 'returns 404' do
    get '/api/v1/users/999999', headers: headers

    expect(response).to have_http_status(:not_found)
  end
end

end

describe 'POST /api/v1/users' do let(:valid_params) { { user: { name: 'Test', email: 'test@example.com' } } }

it 'creates a user' do
  expect {
    post '/api/v1/users', params: valid_params.to_json, headers: headers
  }.to change(User, :count).by(1)

  expect(response).to have_http_status(:created)
end

context 'with invalid params' do
  let(:invalid_params) { { user: { name: '', email: '' } } }

  it 'returns validation errors' do
    post '/api/v1/users', params: invalid_params.to_json, headers: headers

    expect(response).to have_http_status(:unprocessable_entity)
    expect(json_response['errors']).to be_present
  end
end

end

Helper method

def json_response JSON.parse(response.body) end end

API Authentication

Token-Based Auth

app/controllers/api/base_controller.rb

module Api class BaseController < ApplicationController before_action :authenticate_api_user!

private

def authenticate_api_user!
  token = request.headers['Authorization']&#x26;.split(' ')&#x26;.last
  @current_api_user = User.find_by(api_token: token)

  render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_api_user
end

def current_api_user
  @current_api_user
end

end end

JWT Authentication

Using jwt gem

def authenticate_api_user! token = request.headers['Authorization']&.split(' ')&.last return unauthorized unless token

payload = JWT.decode(token, Rails.application.secret_key_base).first @current_api_user = User.find(payload['user_id']) rescue JWT::DecodeError unauthorized end

def unauthorized render json: { error: 'Unauthorized' }, status: :unauthorized end

Workflow Checklist

API Implementation:

  • Define routes in namespace
  • Create base controller with error handling
  • Create version-specific base controller
  • Create resource controller
  • Add authentication (if needed)
  • Write request specs
  • Document API endpoints

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

hotwire-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

i18n-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

solid-queue-setup

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

viewcomponent-patterns

No summary provided by upstream source.

Repository SourceNeeds Review