module Api
module V1
class UsersController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_user, only: [:show, :update, :destroy]
# GET /api/v1/users
def index
@users = User.page(params[:page]).per(params[:per_page] || 20)
render json: {
users: @users.map { |user| UserSerializer.new(user).as_json },
meta: pagination_meta(@users)
}
end
# GET /api/v1/users/:id
def show
render json: UserSerializer.new(@user), status: :ok
end
# POST /api/v1/users
def create
@user = User.new(user_params)
if @user.save
render json: UserSerializer.new(@user), status: :created
else
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end
# PATCH/PUT /api/v1/users/:id
def update
if @user.update(user_params)
render json: UserSerializer.new(@user), status: :ok
else
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end
# DELETE /api/v1/users/:id
def destroy
@user.destroy
head :no_content
end
private
def set_user
@user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'User not found' }, status: :not_found
end
def user_params
params.require(:user).permit(:name, :email, :bio)
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count,
per_page: collection.limit_value
}
end
end
end
end
# app/serializers/user_serializer.rb
class UserSerializer
def initialize(user)
@user = user
end
def as_json
{
id: @user.id,
name: @user.name,
email: @user.email,
bio: @user.bio,
avatar_url: @user.avatar.url,
created_at: @user.created_at.iso8601,
links: {
self: "/api/v1/users/#{@user.id}",
posts: "/api/v1/users/#{@user.id}/posts"
}
}
end
end
# With Active Model Serializers gem
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :bio, :created_at
has_many :posts
belongs_to :company
def created_at
object.created_at.iso8601
end
# Conditional attributes
attribute :email, if: :show_email?
def show_email?
scope.admin? || scope == object
end
end
# Using Jbuilder
# app/views/api/v1/users/show.json.jbuilder
json.user do
json.id @user.id
json.name @user.name
json.email @user.email
json.posts @user.posts do |post|
json.id post.id
json.title post.title
json.url api_v1_post_url(post)
end
end
# app/controllers/concerns/api/error_handling.rb
module Api::ErrorHandling
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
rescue_from StandardError, with: :internal_server_error
end
private
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
def unprocessable_entity(exception)
render json: {
error: 'Validation failed',
details: exception.record.errors.full_messages
}, status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
def internal_server_error(exception)
Rails.logger.error(exception.full_message)
render json: {
error: 'Internal server error'
}, status: :internal_server_error
end
end
# app/controllers/concerns/api/filterable.rb
module Api::Filterable
extend ActiveSupport::Concern
def apply_filters(scope)
filter_params.each do |key, value|
scope = scope.public_send(key, value) if value.present?
end
scope
end
def filter_params
params.slice(:status, :created_after, :created_before, :search)
end
end
# Usage in controller
class Api::V1::PostsController < Api::V1::BaseController
include Api::ErrorHandling
include Api::Filterable
def index
@posts = apply_filters(Post.all)
render json: @posts
end
end
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users do
resources :posts, only: [:index, :create]
member do
post :follow
delete :unfollow
end
collection do
get :search
end
end
resources :posts do
resources :comments, only: [:index, :create, :destroy]
member do
post :publish
post :like
end
end
# Custom routes
post 'auth/login', to: 'authentication#create'
delete 'auth/logout', to: 'authentication#destroy'
get 'me', to: 'users#current'
# Health check
get 'health', to: 'health#show'
end
# API v2
namespace :v2 do
resources :users, only: [:index, :show]
end
end
# Default to API v1
namespace :api, defaults: { format: :json } do
# routes
end
end
Rails conventions support RESTful API development. I use resourceful routing for standard CRUD operations. Controllers inherit from ActionController::API for API-only apps. JSON serialization with Jbuilder or Active Model Serializers structures responses. Versioning uses namespaces—/api/v1/users. Authentication with JWT tokens or OAuth. Pagination with kaminari or pagy gems. Rate limiting protects endpoints. CORS configuration allows cross-origin requests. Error handling returns appropriate HTTP status codes. API documentation with rswag or Swagger. Filtering, sorting, and searching enhance usability. Proper REST design creates intuitive, maintainable APIs. Following conventions reduces decisions and improves consistency.