tessl install github:ThibautBaissac/rails_ai_agents --skill api-versioninggithub.com/ThibautBaissac/rails_ai_agents
Implements RESTful API design with versioning and request specs. Use when building APIs, adding API endpoints, versioning APIs, or when user mentions REST, JSON API, or API design.
Review Score
78%
Validation Score
12/16
Implementation Score
65%
Activation Score
90%
Well-structured APIs need versioning for backwards compatibility and clear organization.
| 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)
# 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
endapp/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# 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# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < Api::BaseController
# V1-specific configuration
end
end
end# 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{
"data": {
"id": 1,
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
}
}
}{
"data": [
{ "id": 1, "type": "user", "attributes": { ... } },
{ "id": 2, "type": "user", "attributes": { ... } }
],
"meta": {
"current_page": 1,
"total_pages": 10,
"total_count": 100
}
}{
"error": "Record not found",
"code": "not_found"
}
{
"errors": {
"email": ["has already been taken"],
"name": ["can't be blank"]
}
}# 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# 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 = 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# 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
endAPI 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