Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dchuk avatar

Api Versioning

  • 2 installs
  • Updated February 9, 2026
  • dchuk/rails_ai_agents

Implements RESTful JSON API design in Rails 8 with versioning for backwards compatibility, plus request tests.

About

Builds versioned RESTful APIs in Rails 8 with request-level tests. A developer uses it when adding API endpoints or versioning a JSON API.

  • Versioning for backwards compatibility and clear organization
  • Request tests for endpoints

Api Versioning by the numbers

  • 2 all-time installs (skills.sh)
  • Ranked #3,765 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dchuk/rails_ai_agents --skill api-versioning

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2
Last updatedFebruary 9, 2026
Repositorydchuk/rails_ai_agents

What it does

Implements RESTful JSON API design in Rails 8 with versioning for backwards compatibility, plus request tests.

Files

SKILL.mdMarkdownGitHub ↗

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

# config/routes.rb
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

# app/controllers/api/base_controller.rb
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

# app/controllers/api/v1/base_controller.rb
module Api
  module V1
    class BaseController < Api::BaseController
    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

API Authentication

Bearer Token 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"]&.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

// Success (single)
{ "data": { "id": 1, "name": "John", "email": "john@example.com" } }

// Success (collection)
{ "data": [...], "meta": { "current_page": 1, "total_pages": 10 } }

// Error
{ "error": "Record not found" }

// Validation errors
{ "errors": { "email": ["has already been taken"] } }

Testing APIs (Minitest)

Request Test Template

# test/controllers/api/v1/users_controller_test.rb
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

  # -- index --
  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

  # -- show --
  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 :not_found
    assert json_response["error"].present?
  end

  # -- create --
  test "POST /api/v1/users creates a user" do
    params = { user: { name: "New User", email: "new@example.com" } }

    assert_difference("User.count", 1) do
      post "/api/v1/users", params: params.to_json, headers: @headers
    end

    assert_response :created
  end

  test "POST /api/v1/users with invalid params returns errors" do
    params = { user: { name: "", email: "" } }

    assert_no_difference("User.count") do
      post "/api/v1/users", params: params.to_json, headers: @headers
    end

    assert_response :unprocessable_entity
    assert json_response["errors"].present?
  end

  # -- update --
  test "PATCH /api/v1/users/:id updates the user" do
    params = { user: { name: "Updated" } }

    patch "/api/v1/users/#{@user.id}", params: params.to_json, headers: @headers

    assert_response :success
    assert_equal "Updated", @user.reload.name
  end

  # -- destroy --
  test "DELETE /api/v1/users/:id destroys the user" do
    assert_difference("User.count", -1) do
      delete "/api/v1/users/#{@user.id}", headers: @headers
    end

    assert_response :no_content
  end

  # -- authentication --
  test "returns 401 without token" do
    get "/api/v1/users", headers: { "Accept" => "application/json" }

    assert_response :unauthorized
  end

  private

  def json_response
    JSON.parse(response.body)
  end

  def api_token_for(user)
    user.sessions.create!.token
  end
end

Checklist

  • [ ] Routes namespaced under api/v1
  • [ ] Base controller with error handling
  • [ ] Authentication configured
  • [ ] Standard response format
  • [ ] Request tests written
  • [ ] 404/422/401 error cases tested
  • [ ] All tests GREEN

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.