Rails7で最小構成からバックエンドAPIを開発する方法まとめ


 

こんにちは。Tomoyuki(@tomoyuki65)です。

私が最初に学び始めたフレームワークとしてはRailsですが、その後の実務ではLaravel(PHP)を使ってきました。

そんな私が久しぶりにRailsを触ってみたいと思ったのと、合わせてRailsの最小構成からAPIを開発する方法を試したかったので、やってみることにしました。

そこでこの記事では、私が久しぶりに試したRails7の最小構成でバックエンドAPIを開発する方法についてまとめておきます。

 

Railsの関連記事👇

SPA構成のWebアプリケーションを開発する方法まとめ【Docker・NextJS(React)・Vercel・Rails7(APIモード)・AWS ECS(Fargate)】

2022年11月22日

Railsチュートリアルをカスタマイズしてポートフォリオを作成する方法【Docker・Rails7・CircleCI対応】

2022年8月15日

Railsチュートリアル(第6版)を進めるための参考資料

2022年7月16日

 



Rails7で最小構成からバックエンドAPIを開発する方法まとめ

まず以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir rails_sample
$ cd rails_sample
$ mkdir api
$ cd api
$ touch Gemfile
$ touch Gemfile.lock
$ touch .env
$ touch compose.yml
$ mkdir docker
$ cd docker
$ mkdir local
$ cd local
$ touch Dockerfile
$ cd ../..

 

source "https://rubygems.org"
gem "rails"

※railsのバージョンを指定していないため、最新版がインストールされます

 

RAILS_ENV=development
BUNDLER_VERSION=2.5.10
LANG=C.UTF-8
TZ=Asia/Tokyo
PORT=3010

 

services:
  api:
    container_name: rails-s-api
    build:
      context: .
      dockerfile: ./docker/local/Dockerfile
      args:
        - RAILS_ENV=${RAILS_ENV}
        - BUNDLER_VERSION=${BUNDLER_VERSION}
        - LANG=${LANG}
        - TZ=${TZ}
        - PORT=${PORT}
    volumes:
      - .:/api
    ports:
      - 3010:${PORT}
    env_file:
      - ./.env
    command: bundle exec puma -C "config/puma.rb"

※環境変数は「.env」で設定し、それを「args:」からDockerfile内へ流しています

 

## ビルドステージ
# 2024年5月時点の最新安定版Rubyの軽量版「alpine」
FROM ruby:3.3.1-alpine AS builder

# 環境変数
ARG RAILS_ENV
ARG BUNDLER_VERSION
ARG LANG
ARG TZ
ARG PORT

# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(ビルド時のみ使う)
    apk add --virtual build-packs --no-cache \
            alpine-sdk \
            build-base \
            curl-dev \
            mysql-dev \
            tzdata

# 作業ディレクトリの指定
WORKDIR /app

# ローカルにあるGemfileとGemfile.lockを
# コンテナ内のディレクトリにコピー
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

# bundlerのバージョンを固定する
RUN gem install bundler -v $BUNDLER_VERSION
RUN bundle -v

# bunlde installを実行する
RUN bundle install --jobs=4

# build-packsを削除
RUN apk del build-packs


## マルチステージビルド
# 2024年5月時点の最新安定版Rubyの軽量版「alpine」
FROM ruby:3.3.1-alpine

# 環境変数
ARG RAILS_ENV
ARG BUNDLER_VERSION
ARG LANG
ARG TZ
ARG PORT

# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(--no-cacheでキャッシュ削除)
    apk add --no-cache \
            bash \
            mysql-dev \
            tzdata \
            gvim \
            # rails newでgitを使用する(ファイル作成後は不要)
            git \
            # M系チップMacの場合に「gcompat」が必要
            gcompat

# 作業ディレクトリの指定
WORKDIR /api

# ビルドステージからファイルをコピー
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . /api

# サーバー起動
EXPOSE 3010
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

※Dockerfileではマルチステージビルドで構築し、コンテナサイズの軽量化を図っています。

 

次に以下のコマンドを実行し、コンテナのビルドおよびRailsファイルの作成を行います。(ファイル作成時に.gitignoreは作成されて欲しいのでgit関連はスキップしていませんが、apiディレクトリ直下に.gitファイルは不要なので最後のコマンドで削除しています。)

$ docker compose build --no-cache
$ docker compose run --rm api rails new . -f -T -d mysql --api \
$ --skip-action-mailer --skip-action-mailbox --skip-action-text \
$ --skip-active-job --skip-active-storage --skip-action-cable \
$ --skip-asset-pipeline --skip-javascript --skip-hotwire --skip-jbuilder
$ rm -r .git

※rails newコマンドのオプションについて、「-f」はファイルを上書き、「-T」はテストコード関連をスキップ、「-d mysql」はDBにMySQLを指定、「–api」はAPIモード(そのままだとCookieやCORS設定などが使えないので注意)、それ以降の–skip〜も不要なものをスキップ。尚、オプションを「-f -T -d mysql –api –minimal」にしてもほぼ同じ結果になりますが、この場合は「bootsnap」もスキップされます。

 

成功すると以下のようにRailsの各種ファイルが作成されます。

 

次に一旦DBのMySQLの設定はスキップしたいため、一時的なDB設定として使えるnulldbのgem「activerecord-nulldb-adapter」をインストールします。

以下のようにファイル「api/Gemfile」の「group :development, :test do」の部分に「gem ‘activerecord-nulldb-adapter’」を追記します。

・・

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ]

  # 一時的なDB設定用にnulldbのgemを追加
  gem "activerecord-nulldb-adapter"
end

・・

 

次に「api/config/database.yml」を以下のように修正します。

・・

default: &default
  # 一時的なDB設定としてnulldbを指定
  # adapter: mysql2
  adapter: nulldb
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

・・

 

次にコンテナを再ビルドし、コンテナを起動します。

$ docker compose build --no-cache
$ docker compose up -d

 

次にブラウザで「http://localhost:3010」にアクセスし、以下のように表示されればOKです。

 

hello worldを出力するAPIを作成

次にhello worldを試すため、テキスト「hello world !!」を出力するAPIを作成してみます。

ファイル「api/app/controllers/application_controller.rb」、「api/config/routes.rb」それぞれ以下のように修正します。

class ApplicationController < ActionController::API
  def hello
    render plain: "hello world !!"
  end
end

 

Rails.application.routes.draw do

・・

  # API用のルーティング設定
  scope "api"do
    scope "v1"do
      get "/hello", to: "application#hello"
    end
  end
end

 

次にブラウザで「http://localhost:3010/api/v1/hello」にアクセスし、以下のように表示されればOKです。

 



テストコード用にRSpecの設定

次にテストを実施できるようにするため、RSpecを導入します。

以下のようにファイル「api/Gemfile」の「group :development, :test do」の部分にテスト関連のgemを追加します。

・・

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ]

  # 一時的なDB設定用にnulldbのgemを追加
  gem "activerecord-nulldb-adapter"

  # テスト用のGemを「group :development, :test」内に追加
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem "shoulda-matchers"
end

・・

 

次に以下のコマンドを実行し、RSpecをインストールします。

$ docker compose exec api bundle install
$ docker compose exec api rails g rspec:install

 

次にRSpecの初期設定として「api/.rspec」に「–color」と「–format documentation」を追加します。

--require spec_helper
--color
--format documentation

 

次に以下のコマンドを実行し、テスト実行用コマンドのエイリアスを設定します。

$ docker compose exec api bundle binstubs rspec-core

 

次に不要なspecファイルが無駄に作られないようにするため、「api/config/application.rb」にジェネレータの設定を追加します。

module Api
  class Application < Rails::Application

・・・

  # ジェネレータの設定を追加
  config.generators do |g|
    g.test_framework :rspec,
      fixtures: false,
      helper_specs: false,
      view_specs: false,
      routing_specs: false
    end
  end
end

 

次に環境変数に「RAILS_ENV=development」を設定した場合はRSpecの実行時の環境変数も「development」になってしまうため、「api/spec/rails_helper.rb」を以下のように修正します。

・・・

# RSpec実行時の環境変数「RAILS_ENV」の値が「test」になるように修正
# ENV['RAILS_ENV'] ||= 'test'
ENV['RAILS_ENV'] = 'test'

・・・

 

次に以下のコマンドを実行し、RSpecを実行して確認します。

$ docker compose exec api bin/rspec

※binstubを設定していない場合はコマンド「bundle exec rspec」を使います。

 

テスト実行後、以下のようにRSpecが実行できればOKです。

 

RSpecでhello worldを出力するAPIのテストコードを作成

次に上記で作成したAPIのテストコードを作成するため、以下のコマンドを実行してテストコード用のファイルを作成します。

$ docker compose exec api rails g rspec:request application

 

次にファイル「api/spec/requests/applications_spec.rb」を以下のように修正します。

require 'rails_helper'

RSpec.describe "Applications", type: :request do
  # テスト用APIの検証
  describe "GET /api/v1/hello" do
    # レスポンスのステータス検証
    it "ステータス200で正常終了すること" do
      get hello_path
      expect(response).to have_http_status(:success)
    end

    # レスポンスの出力内容の検証
    it "「hello world !!」を出力すること" do
      get hello_path
      expect(response.body).to eq("hello world !!")
    end
  end
end

 

次に以下のコマンドを実行し、RSpecを実行してテスト結果を確認します。

$ docker compose exec api bin/rspec

 

テスト実行後、以下のような結果になればOKです。

 

Railsのタイムゾーンを日本に変更する

次にRailsのタイムゾーンを日本(Asia/Tokyo)に変更するため、「api/config/application.rb」に設定を追加します。

module Api
  class Application < Rails::Application

・・・

  # タイムゾーン設定を日本にする
  config.time_zone = "Asia/Tokyo"
  config.active_record.default_timezone = :local
end

 

次に以下のコマンドを実行し、コンテナを再起動します。

$ docker compose down
$ docker compose up -d

 



RailsのAPIモードでCookieを使えるようにする

RailsのAPIモードはデフォルトではCookieを使えないため、使えるようにしておきます。

まずはファイル「api/Gemfile」でコメントアウトされている「gem “rack-cors”」の部分をコメントを外して有効にします。

・・

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
gem "rack-cors"

・・

 

次に以下のコマンドを実行し、gemをインストールします。

$ docker compose exec api bundle install

 

次にファイル「api/config/initializers/cors.rb」、「api/config/application.rb」、「api/app/controllers/application_controller.rb」をそれぞれ以下のように修正します。

・・・

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "http://localhost:3000"

    resource "/api/v1/*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true
  end
end

※api/config/initializers/cors.rbはクロスオリジンに関する設定ファイルで、originsにはフロントエンド側のドメインを設定します。

 

次に「Cookies」を使えるようにするため、「api/config/application.rb」に設定を追加します。

module Api
  class Application < Rails::Application

・・・

  # Cookiesを利用するための設定
  config.middleware.use ActionDispatch::Cookies
  config.action_dispatch.cookies_same_site_protection = :none
end

 

次にファイル「api/app/controllers/application_controller.rb」で「ActionController::Cookies」読み込み、上記で作成したAPIでCookieを設定するよう修正します。

class ApplicationController < ActionController::API
  # Cookiesを読み込む
  include ActionController::Cookies

  def hello
    # レスポンスにCookieを設定
    cookies[:hello] = {
      value: "hello",
      expires: 90.day.from_now,
      path: '/',
      httponly: true,
      secure: true
    }

    render plain: "hello world !!"
  end
end

 

次にブラウザで「http://localhost:3010/api/v1/hello」にアクセスし、ブラウザの開発者用モードで「Application > Cookies > http://localhost:3010」を確認してCookieがあればOKです。

 



DBとしてMySQLを追加する

次にDBとしてMySQLを追加します。ファイル「api/.env」、「api/compose.yml」、「api/config/database.yml」をそれぞれ以下のように修正します。

・・・


MYSQL_ROOT_PASSWORD=password

※api/.envにMySQLのルートパスワードの設定を追加します。

 

services:
  api:
    container_name: rails-s-api
    build:
      context: .
      dockerfile: ./docker/local/Dockerfile
      args:
        - RAILS_ENV=${RAILS_ENV}
        - BUNDLER_VERSION=${BUNDLER_VERSION}
        - LANG=${LANG}
        - TZ=${TZ}
        - PORT=${PORT}
    volumes:
      - .:/api
    ports:
      - 3010:${PORT}
    env_file:
      - ./.env
    depends_on:
      - db
    command: bundle exec puma -C "config/puma.rb"
  # DBに関する設定
  db:
    container_name: rails-s-db
    image: mysql:8.0.36
    env_file:
      - ./.env
    ports:
      - 3306:3306
    volumes:
      - ./tmp/db:/var/lib/mysql

※api/compose.ymlにDB用のコンテナ設定を追加します。

 

・・・

default: &default
  # adapterを初期設定に戻す
  adapter: mysql2
  encoding: utf8mb4
  # コレクションを指定
  collation: utf8mb4_bin
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  # 以下をコメントアウト
  # username: root
  # password:
  # host: localhost

development:
  <<: *default
  database: api_development
  # DBへの接続情報を追加
  username: root
  password: <%= ENV["MYSQL_ROOT_PASSWORD"] %>
  host: db

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: api_test
  # DBへの接続情報を追加
  username: root
  password: <%= ENV["MYSQL_ROOT_PASSWORD"] %>
  host: db

・・・

※api/config/database.ymlにDBへの接続設定を追加修正します。

 

次に以下のコマンドを実行し、コンテナを再起動します。

$ docker compose down
$ docker compose up -d

 

次に以下のコマンドを実行し、DBを作成します。

$ docker compose exec api rails db:create

 

次にブラウザで「http://localhost:3010」にアクセスし、エラーにならず正常に表示されればOKです。

 



ユーザーテーブルとデータを操作するAPIを作成する

次はユーザーテーブルを作成し、データを操作するCRUD処理のAPIを作成してみます。

データ削除時は論理削除を行いたいため、まずはGemfileにgem「discard」を追加します。

・・・

# 論理削除用
gem "discard"

・・・

 

次に以下のコマンドを実行してgemをインストールします。

$ docker compose exec api bundle install

 

次に以下のコマンドを実行し、ユーザーテーブル用の各種ファイルを作成します。

$ docker compose exec api rails g model User uid:string member_id:integer last_name:string first_name:string email:string discarded_at:datetime

 

次に作成されたマイグレーションファイルとモデルファイルについて、それぞれ以下のように修正します。

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :uid, null: false
      t.integer :member_id
      t.string :last_name, null: false
      t.string :first_name, null: false
      t.string :email, null: false

      t.timestamps
      t.datetime :discarded_at
    end
    add_index :users, :uid, unique: true
    add_index :users, :member_id, unique: true
    add_index :users, :email, unique: true
    add_index :users, :discarded_at
    add_index :users, [:email, :discarded_at], unique: true, name: 'unique_email_discarded_at'
  end
end

 

class User < ApplicationRecord
  # 論理削除設定
  include Discard::Model
  default_scope -> { kept }

  # バリデーション
  validates :uid, presence: true, uniqueness: true, length: { maximum: 255 }
  validates :member_id, uniqueness: true, length: { maximum: 9 }
  validates :last_name, presence: true, length: { maximum: 255 }
  validates :first_name, presence: true, length: { maximum: 255 }
  validates :email, presence: true, uniqueness: { scope: :discarded_at }, length: { maximum: 255 }

  # 日付項目のフォーマット変換
  def f_created_at
    created_at.strftime('%Y/%m/%d %H:%M:%S')
  end

  def f_updated_at
    updated_at.strftime('%Y/%m/%d %H:%M:%S')
  end

  def f_discarded_at
    if discarded_at
      discarded_at.strftime('%Y/%m/%d %H:%M:%S')
    else
      nil
    end
  end
end

 

次に以下のコマンドを実行し、マイグレーションを実行します。

$ docker compose exec api rails db:migrate
$ docker compose exec api rails db:migrate RAILS_ENV=test

 

次に以下のコマンドを実行し、サービス層を構築するための各種ファイルを作成します。

$ cd app
$ mkdir services
$ cd services
$ mkdir user
$ cd user
$ touch create_user_service.rb
$ touch get_a_user_service.rb
$ touch update_user_service.rb
$ touch delete_user_service.rb
$ touch get_users_with_discarded_service.rb
$ cd ../../..

※業務ロジックを全てコントローラーに書くのはよくないので、サービス層を作ってコントローラーから呼び出すようにします。ただし、全てをサービス層に寄せるのもよくないので、処理の流れやDBのやり取りなどはサービス層に寄せつつ、コアなルールはモデルに寄せるようにした方がいいです。

 

次に作成した各種サービスファイルをそれぞれ以下のように記述します。

class User::CreateUserService

  def self.call(...)
    new(...).call
  end

  def initialize(params)
    @params = params
  end

  def call
    @user = User.new(@params)

    # トランザクション
    ActiveRecord::Base.transaction do
      # 一時保存
      @user.save!

      # 会員IDの設定(9桁)
      @member_id = 100000000 + @user.id
      @user.member_id = @member_id
      @user.save!
    end

    # 戻り値
    @user

  rescue => e
    nil
  end
end

 

class User::GetAUserService

  def self.call(...)
    new(...).call
  end

  def initialize(uid)
    @uid = uid
  end

  def call
    @user = User.where(uid: @uid).first
  end
end

 

class User::UpdateUserService

  def self.call(...)
    new(...).call
  end

  def initialize(params, uid)
    @params = params
    @uid = uid
  end

  def call
    @user = User.where(uid: @uid).first

    # トランザクション
    ActiveRecord::Base.transaction do
      @user.update!(@params)
    end

    # 戻り値
    @user

  rescue => e
    nil
  end
end

 

class User::DeleteUserService

  def self.call(...)
    new(...).call
  end

  def initialize(uid)
    @uid = uid
  end

  def call
    ActiveRecord::Base.transaction do
      @user = User.where(uid: @uid).first
      @user.discard!
    end

    # 戻り値
    @user

  rescue => e
    nil
  end
end

 

class User::GetUsersWithDiscardedService

  def self.call(...)
    new(...).call
  end

  def initialize()
  end

  def call
    @users = User.with_discarded
  end
end

 

次に以下のコマンドを実行し、コンテナを再起動します。

$ docker compose down
$ docker compose build --no-cache 
$ docker compose up -d

 

次に以下のコマンドを実行し、ユーザーコントローラーを作成します。

$ docker compose exec api rails g controller users

 

次に作成したユーザーコントローラーを次のように記述します。

class UsersController < ApplicationController
  def create
    # サービスを実行
    res = User::CreateUserService.call(create_params)

    # 実行結果によりレスポンスを設定
    if res
      render json: res, status: :created
    else
      render json: { message: "会員登録ができませんでした。" }, status: :internal_server_error
    end
  end

  def get_user
    # パスパラメータの取得
    uid = params[:uid]

    # サービスを実行
    user = User::GetAUserService.call(uid)

    # レスポンス設定
    render json: user
  end

  def update
    # パスパラメータの取得
    uid = params[:uid]

    # サービスを実行
    res = User::UpdateUserService.call(update_params, uid)

    # 実行結果によりレスポンスを設定
    if res
      render json: res
    else
      render json: { message: "会員情報の更新ができませんでした。" }, status: :internal_server_error
    end
  end

  def delete
    # パスパラメータの取得
    uid = params[:uid]

    # サービスを実行
    res = User::DeleteUserService.call(uid)

    # 実行結果によりレスポンスを設定
    if res
      render json: { message: "OK" }, status: :ok
    else
      render json: { message: "退会処理ができませんでした。" }, status: :internal_server_error
    end
  end

  def get_users_with_discarded
    # サービスを実行
    users = User::GetUsersWithDiscardedService.call()

    # レスポンス設定
    render json: users
  end

  private
    # ストロングパラメータ
    def create_params
      params.require(:user).permit(:uid, :last_name, :first_name, :email)
    end

    def update_params
      params.require(:user).permit(:last_name, :first_name, :email)
    end
end

※Railsでは登録や更新時には対象の項目以外を更新させないようにするため、ストロングパラメータを使います。

 

次にルーティングファイルにルートを追加します。

Rails.application.routes.draw do

・・

  # API用のルーティング設定
  scope "api"do
    scope "v1"do
      get "/hello", to: "application#hello"

      post "/user", to: "users#create"
      get "/user/:uid", to: "users#get_user"
      put "/user/:uid", to: "users#update"
      delete "/user/:uid", to: "users#delete"
      get "/users/with-discarded", to: "users#get_users_with_discarded"
    end
  end
end

 

これでCRUD処理をするAPIの準備が整ったので、Postmanを使って試してみます。(Postmanについての詳細は割愛させていただきます。)

まずはユーザー作成用の「http://localhost:3010/api/v1/user」をPOSTで実行し、下図のように正常終了すればOKです。

 

次にユーザー取得用の「http://localhost:3010/api/v1/user/XYZ123abc1」をGETで実行し、下図のようにユーザー情報が取得できればOKです。

 

次にユーザー情報更新用の「http://localhost:3010/api/v1/user/XYZ123abc1」をPUTで実行し、下図のように正常終了すればOKです。

 

次にもう一度ユーザー取得用の「http://localhost:3010/api/v1/user/XYZ123abc1」をGETで実行し、下図のように対象項目が更新されていればOKです。

 

次にユーザーを論理削除する「http://localhost:3010/api/v1/user/XYZ123abc1」をDELETEで実行し、下図のように正常終了すればOKです。

 

次にもう一度ユーザー取得用の「http://localhost:3010/api/v1/user/XYZ123abc1」をGETで実行し、対象データが存在せずnullが取得できればOKです。

 

次に削除済みデータも含めて全てのユーザーを取得する「http://localhost:3010/api/v1/users/with-discarded」をGETで実行し、下図のようにユーザー情報が取得でき、discarded_atに削除日が設定されていればOKです。

 



最後に

今回はRails7の最小構成でバックエンドAPIを開発する方法についてまとめました。

久しぶりに触ってみた感じ、やはりRailsは色々なところが自動化されていて、使いやすいフレームワークだなと改めて思いました。

また私自身が実務経験を経てレベルアップしたこともあり、以前よりもいい感じでコードを書けるようになってきたのが実感できて良かったです。

過去にもRailsの記事は書いてきましたが、今回ご紹介した方法の方がシンプルでいい感じに作り始めることができると思うので、これから試す方はぜひ参考にしてみて下さい!

 

各種SNSなど

各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします!

 

The following two tabs change content below.

Tomoyuki

SEを5年経験後、全くの未経験ながら思い切ってブロガーに転身し、月間13万PVを達成。その後コロナの影響も受け、以前から興味があったWeb系エンジニアへのキャリアチェンジを決意。現在はWeb系エンジニアとして働きながら、プロゲーマーとしても活躍できるように活動中。








シェアはこちらから


【2024年】おすすめのゲーミングPC

モンハンワイルズの発売日とPC版(Steam版)の推薦スペックが公開されたので、おすすめのゲーミングPCをご紹介!


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です