RESTful API 完全ガイド - nyg1971/business_api GitHub Wiki

RESTful API 完全ガイド

🎯 REST API とは

基本概念

REST = REpresentational State Transfer

  • リソース指向の設計思想
  • HTTPメソッドでCRUD操作を表現
  • ステートレス通信
  • 統一されたインターフェース

API = Application Programming Interface

  • アプリケーション間のデータやり取りの仕組み
  • JSON形式でのデータ交換
  • プラットフォーム非依存

🏗️ RESTful設計の原則

1. リソース指向

# ✅ 良い設計(リソース中心)
/api/v1/users           # ユーザーリソース
/api/v1/posts           # 投稿リソース
/api/v1/comments        # コメントリソース

# ❌ 悪い設計(アクション中心)
/api/v1/getUsers        # 動詞を含む
/api/v1/createPost      # 動詞を含む
/api/v1/deleteComment   # 動詞を含む

2. HTTPメソッドの意味

メソッド 意味 安全性 冪等性
GET 取得 GET /users
POST 作成 POST /users
PUT 更新(全体) PUT /users/1
PATCH 更新(部分) PATCH /users/1
DELETE 削除 DELETE /users/1

3. ステートレス通信

# 各リクエストが独立している
# サーバーはクライアントの状態を保持しない
# 認証情報は毎回送信(JWT トークンなど)

GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

📋 標準的なRESTful APIパターン

ユーザー管理API

# ユーザー一覧取得
GET /api/v1/users
Response: [{"id": 1, "name": "太郎"}, {"id": 2, "name": "花子"}]

# 特定ユーザー取得
GET /api/v1/users/1
Response: {"id": 1, "name": "太郎", "email": "[email protected]"}

# ユーザー作成
POST /api/v1/users
Body: {"name": "次郎", "email": "[email protected]"}
Response: {"id": 3, "name": "次郎", "email": "[email protected]"}

# ユーザー更新
PUT /api/v1/users/1
Body: {"name": "太郎", "email": "[email protected]"}
Response: {"id": 1, "name": "太郎", "email": "[email protected]"}

# ユーザー削除
DELETE /api/v1/users/1
Response: {"message": "User deleted successfully"}

認証API(今回実装したもの)

# ユーザー登録
POST /api/v1/auth/signup
Body: {"user": {"email": "[email protected]", "password": "password123"}}
Response: {"token": "eyJ...", "user": {"id": 1, "email": "[email protected]"}}

# ログイン
POST /api/v1/auth/login
Body: {"email": "[email protected]", "password": "password123"}
Response: {"token": "eyJ...", "user": {"id": 1, "email": "[email protected]"}}

# 認証済みユーザー情報取得
GET /api/v1/auth/me
Headers: Authorization: Bearer eyJ...
Response: {"user": {"id": 1, "email": "[email protected]", "role": "staff"}}

🔗 リソース間の関係表現

ネストしたリソース

# ユーザーの投稿一覧
GET /api/v1/users/1/posts

# 投稿のコメント一覧
GET /api/v1/posts/1/comments

# 特定ユーザーの特定投稿
GET /api/v1/users/1/posts/5

関連リソースの取得

# クエリパラメータでの関連取得
GET /api/v1/posts?include=author,comments
GET /api/v1/users?filter[role]=admin
GET /api/v1/posts?sort=created_at&order=desc

📊 HTTPステータスコードの使い分け

成功レスポンス (2xx)

200 OK          # 正常取得・更新
201 Created     # 正常作成
204 No Content  # 正常削除(レスポンスボディなし)

クライアントエラー (4xx)

400 Bad Request          # 不正なリクエスト
401 Unauthorized         # 認証が必要
403 Forbidden           # 権限不足
404 Not Found           # リソースが存在しない
422 Unprocessable Entity # バリデーションエラー

サーバーエラー (5xx)

500 Internal Server Error # サーバー内部エラー
503 Service Unavailable   # サービス利用不可

🎨 JSON レスポンス設計

成功レスポンスの例

// 単一リソース
{
  "user": {
    "id": 1,
    "name": "太郎",
    "email": "[email protected]",
    "created_at": "2025-06-18T10:00:00Z"
  }
}

// 複数リソース + メタデータ
{
  "users": [
    {"id": 1, "name": "太郎"},
    {"id": 2, "name": "花子"}
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "per_page": 20
  }
}

エラーレスポンスの例

// バリデーションエラー
{
  "errors": [
    "Email has already been taken",
    "Password is too short (minimum is 6 characters)"
  ]
}

// 認証エラー
{
  "error": "Unauthorized",
  "message": "Invalid or expired token"
}

// 詳細なエラー情報
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request data is invalid",
    "details": {
      "email": ["has already been taken"],
      "password": ["is too short"]
    }
  }
}

🔧 Rails での RESTful API 実装

ルーティング設計

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      # 認証系
      post 'auth/signup', to: 'auth#signup'
      post 'auth/login', to: 'auth#login'
      get 'auth/me', to: 'auth#me'
      
      # リソース系
      resources :users do
        resources :posts, only: [:index, :show, :create]
      end
      
      resources :posts do
        resources :comments, except: [:show]
      end
    end
  end
end

コントローラー実装パターン

# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < Api::V1::BaseController
  before_action :set_user, only: [:show, :update, :destroy]
  before_action :authorize_admin, only: [:destroy]

  def index
    users = User.all.order(:created_at)
    render json: { 
      users: users.as_json(only: [:id, :name, :email, :created_at]),
      meta: { total: users.count }
    }
  end

  def show
    render json: { user: @user.as_json(include: :profile) }
  end

  def create
    user = User.new(user_params)
    if user.save
      render json: { user: user }, status: :created
    else
      render json: { errors: user.errors.full_messages }, 
             status: :unprocessable_entity
    end
  end

  def update
    if @user.update(user_params)
      render json: { user: @user }
    else
      render json: { errors: @user.errors.full_messages }, 
             status: :unprocessable_entity
    end
  end

  def destroy
    @user.destroy
    render json: { message: 'User deleted successfully' }, status: :ok
  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, :role)
  end
end

🔐 認証・認可パターン

JWT認証実装

# app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ApplicationController
  before_action :authenticate_request

  private

  def authenticate_request
    header = request.headers['Authorization']
    if header.present?
      token = header.split(' ').last
      begin
        decoded = JsonWebToken.decode(token)
        @current_user = User.find(decoded[:user_id])
      rescue JWT::DecodeError
        render json: { error: 'Unauthorized' }, status: :unauthorized
      end
    else
      render json: { error: 'Unauthorized' }, status: :unauthorized
    end
  end

  def current_user
    @current_user
  end
end

権限制御

# 役職別アクセス制御
def authorize_admin
  unless current_user.admin?
    render json: { error: 'Forbidden' }, status: :forbidden
  end
end

def authorize_owner_or_admin(resource)
  unless resource.user == current_user || current_user.admin?
    render json: { error: 'Forbidden' }, status: :forbidden
  end
end

📡 API バージョニング

URL バージョニング(推奨)

/api/v1/users    # バージョン1
/api/v2/users    # バージョン2(新機能追加)

ヘッダーバージョニング

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

後方互換性の保持

# v1: 従来の形式
{
  "id": 1,
  "name": "太郎"
}

# v2: 新しい形式(v1も継続サポート)
{
  "id": 1,
  "name": "太郎",
  "first_name": "太郎",
  "last_name": "田中",
  "full_name": "田中太郎"  # 新フィールド
}

🧪 API テスト

RSpec Request Spec例

# spec/requests/api/v1/users_spec.rb
RSpec.describe 'Api::V1::Users', type: :request do
  let(:user) { create(:user) }
  let(:admin) { create(:user, :admin) }
  let(:headers) { { 'Authorization' => "Bearer #{jwt_token(user)}" } }

  describe 'GET /api/v1/users' do
    it 'returns users list' do
      create_list(:user, 3)
      
      get '/api/v1/users', headers: headers
      
      expect(response).to have_http_status(:ok)
      expect(JSON.parse(response.body)['users']).to have(4).items
    end
  end

  describe 'POST /api/v1/users' do
    let(:valid_params) do
      {
        user: {
          name: 'New User',
          email: '[email protected]'
        }
      }
    end

    context 'with valid parameters' do
      it 'creates a new user' do
        post '/api/v1/users', params: valid_params, headers: headers, as: :json
        
        expect(response).to have_http_status(:created)
        expect(User.count).to eq(2)
      end
    end

    context 'with invalid parameters' do
      let(:invalid_params) { { user: { name: '' } } }

      it 'returns validation errors' do
        post '/api/v1/users', params: invalid_params, headers: headers, as: :json
        
        expect(response).to have_http_status(:unprocessable_entity)
        expect(JSON.parse(response.body)).to have_key('errors')
      end
    end
  end
end

📚 フロントエンド連携例

JavaScript/React での使用

// APIクライアントクラス
class ApiClient {
  constructor(baseURL = '/api/v1') {
    this.baseURL = baseURL;
    this.token = localStorage.getItem('token');
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: {
        'Content-Type': 'application/json',
        ...(this.token && { 'Authorization': `Bearer ${this.token}` }),
        ...options.headers,
      },
      ...options,
    };

    const response = await fetch(url, config);
    
    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`);
    }
    
    return response.json();
  }

  // 認証
  async login(email, password) {
    const response = await this.request('/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
    });
    
    this.token = response.token;
    localStorage.setItem('token', this.token);
    return response;
  }

  // CRUD操作
  async getUsers() {
    return this.request('/users');
  }

  async createUser(userData) {
    return this.request('/users', {
      method: 'POST',
      body: JSON.stringify({ user: userData }),
    });
  }

  async updateUser(id, userData) {
    return this.request(`/users/${id}`, {
      method: 'PUT',
      body: JSON.stringify({ user: userData }),
    });
  }

  async deleteUser(id) {
    return this.request(`/users/${id}`, {
      method: 'DELETE',
    });
  }
}

// 使用例
const api = new ApiClient();

// ログイン
await api.login('[email protected]', 'password');

// ユーザー一覧取得
const users = await api.getUsers();

// ユーザー作成
const newUser = await api.createUser({
  name: 'New User',
  email: '[email protected]'
});

🎯 RESTful API設計のベストプラクティス

1. 一貫性のある命名

# ✅ 推奨
/api/v1/users
/api/v1/user_profiles
/api/v1/blog_posts

# ❌ 非推奨
/api/v1/Users           # 大文字
/api/v1/userProfiles    # キャメルケース
/api/v1/blog-posts      # ハイフン

2. 適切なHTTPメソッド使用

# ✅ 正しい使い方
GET    /posts           # 安全で冪等
POST   /posts           # 非安全、非冪等
PUT    /posts/1         # 非安全、冪等
DELETE /posts/1         # 非安全、冪等

# ❌ 間違った使い方
GET    /posts/delete/1  # GETで削除は不適切
POST   /posts/1         # 更新にPOSTは不適切

3. 適切なステータスコード

# 成功時
render json: data, status: :ok          # 200
render json: data, status: :created     # 201
head :no_content                        # 204

# エラー時
render json: errors, status: :bad_request           # 400
render json: errors, status: :unauthorized          # 401
render json: errors, status: :forbidden             # 403
render json: errors, status: :not_found             # 404
render json: errors, status: :unprocessable_entity  # 422

4. エラーハンドリングの統一

# app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
  rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
  rescue_from ActionController::ParameterMissing, with: :parameter_missing

  private

  def record_not_found(error)
    render json: { 
      error: 'Record not found',
      message: error.message 
    }, status: :not_found
  end

  def record_invalid(error)
    render json: { 
      errors: error.record.errors.full_messages 
    }, status: :unprocessable_entity
  end

  def parameter_missing(error)
    render json: { 
      error: 'Missing required parameter',
      parameter: error.param 
    }, status: :bad_request
  end
end

ポイント

技術要素

  1. RESTful API設計原則

    • リソース指向設計
    • 適切なHTTPメソッド使用
    • ステートレス通信
  2. JSON API実装

    • 構造化されたレスポンス設計
    • エラーハンドリングの統一
    • バリデーション実装
  3. セキュリティ対策

    • JWT認証実装
    • CORS設定
    • Strong Parameters
    • 役職別権限制御
  4. テスト実装

    • Request Spec作成
    • 認証フローテスト
    • エラーケーステスト

RESTful APIは現代のWebアプリケーション開発における標準的な設計手法で、マイクロサービス、SPA、モバイルアプリなど幅広い分野で活用されている。 今回実装したシステムは、実際の業務で求められるAPI開発の実装を包括的にカバーしています。