
api-endpoint
Generate JSON API endpoints with authentication and versioning. Use when user needs API, mobile back
by GangWooLee|Open Source
API Endpoint Generator
Generate RESTful JSON API endpoints with authentication, versioning, and documentation.
Quick Start
Task Progress (copy and check off):
- [ ] 1. Design API endpoint structure
- [ ] 2. Create API controller
- [ ] 3. Add routes with versioning
- [ ] 4. Implement authentication
- [ ] 5. Add serialization
- [ ] 6. Handle errors
- [ ] 7. Test with curl/Postman
- [ ] 8. Document endpoints
API Structure
app/controllers/api/
├── v1/
│ ├── base_controller.rb
│ ├── posts_controller.rb
│ ├── users_controller.rb
│ └── sessions_controller.rb
└── v2/
└── ...
Base Controller
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_api_user
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
private
def authenticate_api_user
token = request.headers["Authorization"]&.split(" ")&.last
@current_api_user = User.find_by(api_token: token)
unless @current_api_user
render json: { error: "Unauthorized" }, status: :unauthorized
end
end
def current_api_user
@current_api_user
end
def not_found
render json: { error: "Resource not found" }, status: :not_found
end
def unprocessable_entity(exception)
render json: { errors: exception.record.errors.full_messages }, status: :unprocessable_entity
end
end
end
end
Resource Controller
# app/controllers/api/v1/posts_controller.rb
module Api
module V1
class PostsController < BaseController
before_action :set_post, only: [:show, :update, :destroy]
# GET /api/v1/posts
def index
@posts = Post.includes(:user).order(created_at: :desc).limit(50)
render json: {
data: @posts.map { |post| post_json(post) },
meta: {
total: @posts.count,
page: 1,
per_page: 50
}
}
end
# GET /api/v1/posts/:id
def show
render json: { data: post_json(@post) }
end
# POST /api/v1/posts
def create
@post = current_api_user.posts.build(post_params)
if @post.save
render json: { data: post_json(@post) }, status: :created
else
render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
end
end
# PATCH /api/v1/posts/:id
def update
if @post.user != current_api_user
return render json: { error: "Forbidden" }, status: :forbidden
end
if @post.update(post_params)
render json: { data: post_json(@post) }
else
render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
end
end
# DELETE /api/v1/posts/:id
def destroy
if @post.user != current_api_user
return render json: { error: "Forbidden" }, status: :forbidden
end
@post.destroy
head :no_content
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :content, :status)
end
def post_json(post)
{
id: post.id,
title: post.title,
content: post.content,
status: post.status,
likes_count: post.likes_count,
comments_count: post.comments_count,
created_at: post.created_at.iso8601,
updated_at: post.updated_at.iso8601,
user: {
id: post.user.id,
name: post.user.name,
avatar_url: post.user.avatar_url
}
}
end
end
end
end
Routes
# config/routes.rb
namespace :api do
namespace :v1 do
resources :posts, only: [:index, :show, :create, :update, :destroy]
resources :users, only: [:show, :create, :update]
post "sessions", to: "sessions#create"
delete "sessions", to: "sessions#destroy"
end
end
Authentication
Token-Based
Generate Token:
# app/models/user.rb
class User < ApplicationRecord
has_secure_token :api_token
def regenerate_api_token
regenerate_api_token
end
end
Migration:
add_column :users, :api_token, :string
add_index :users, :api_token, unique: true
Login Endpoint:
# app/controllers/api/v1/sessions_controller.rb
module Api
module V1
class SessionsController < BaseController
skip_before_action :authenticate_api_user, only: [:create]
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
render json: {
data: {
token: user.api_token,
user: user_json(user)
}
}, status: :created
else
render json: { error: "Invalid credentials" }, status: :unauthorized
end
end
def destroy
current_api_user.regenerate_api_token
head :no_content
end
private
def user_json(user)
{
id: user.id,
email: user.email,
name: user.name
}
end
end
end
end
Client Usage:
# Login
curl -X POST http://localhost:3000/api/v1/sessions \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "password"}'
# Use token
curl http://localhost:3000/api/v1/posts \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
Response Format
Success (200/201)
{
"data": {
"id": 1,
"title": "Post Title",
"content": "Content here"
}
}
List (200)
{
"data": [
{ "id": 1, "title": "Post 1" },
{ "id": 2, "title": "Post 2" }
],
"meta": {
"total": 100,
"page": 1,
"per_page": 50
}
}
Error (4xx/5xx)
{
"error": "Resource not found"
}
Validation Error (422)
{
"errors": [
"Title can't be blank",
"Content is too short"
]
}
Pagination
def index
page = params[:page]&.to_i || 1
per_page = params[:per_page]&.to_i || 20
per_page = 100 if per_page > 100 # Max limit
@posts = Post.order(created_at: :desc)
.limit(per_page)
.offset((page - 1) * per_page)
render json: {
data: @posts.map { |post| post_json(post) },
meta: {
total: Post.count,
page: page,
per_page: per_page,
total_pages: (Post.count.to_f / per_page).ceil
}
}
end
Serialization (Optional)
Using Jbuilder:
# app/views/api/v1/posts/index.json.jbuilder
json.data @posts do |post|
json.id post.id
json.title post.title
json.content post.content
json.user do
json.id post.user.id
json.name post.user.name
end
end
json.meta do
json.total @posts.count
end
Controller:
def index
@posts = Post.includes(:user).limit(50)
# Renders app/views/api/v1/posts/index.json.jbuilder
end
CORS (for JavaScript clients)
Gemfile:
gem 'rack-cors'
config/initializers/cors.rb:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:3001', 'example.com' # Your frontend domains
resource '/api/*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
Testing
RSpec (optional):
# spec/requests/api/v1/posts_spec.rb
RSpec.describe "Api::V1::Posts", type: :request do
let(:user) { create(:user) }
let(:headers) { { "Authorization" => "Bearer #{user.api_token}" } }
describe "GET /api/v1/posts" do
it "returns posts" do
create_list(:post, 3)
get "/api/v1/posts", headers: headers
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)["data"].count).to eq(3)
end
end
describe "POST /api/v1/posts" do
it "creates a post" do
post_params = { title: "New Post", content: "Content here" }
post "/api/v1/posts", params: { post: post_params }, headers: headers
expect(response).to have_http_status(:created)
expect(JSON.parse(response.body)["data"]["title"]).to eq("New Post")
end
end
end
Documentation
README section:
## API Documentation
### Authentication
All API requests require authentication via Bearer token.
```bash
Authorization: Bearer YOUR_TOKEN_HERE
Endpoints
GET /api/v1/posts
- Returns list of posts
- Params:
page,per_page
POST /api/v1/posts
- Create new post
- Body:
{ "post": { "title": "...", "content": "..." } }
GET /api/v1/posts/:id
- Returns single post
PATCH /api/v1/posts/:id
- Update post (owner only)
DELETE /api/v1/posts/:id
- Delete post (owner only)
## Best Practices
1. **Versioning**: Always use `/api/v1/` namespace
2. **Authentication**: Token in `Authorization` header
3. **Consistent Format**: `{ data: {}, meta: {} }`
4. **Error Handling**: Proper HTTP status codes
5. **N+1 Prevention**: Use `includes()` in queries
6. **Rate Limiting**: Consider adding (e.g., rack-attack gem)
7. **HTTPS Only**: In production
## Checklist
- [ ] API controller created in `app/controllers/api/v1/`
- [ ] Inherits from BaseController
- [ ] Routes added with `namespace :api, :v1`
- [ ] Authentication implemented
- [ ] JSON responses with `data` and `meta`
- [ ] Error handling for 404, 422, 401, 403
- [ ] CORS configured if needed
- [ ] Tested with curl or Postman
- [ ] API documented