💎

Ruby on Railsで始めるWeb API実装

2023/12/19に公開

はじめに

この記事は、一関高専Advent Calender2023 2日目の記事です。

筆者がRuby on Railsに出会ったのは2年生の頃でしたが、プログラミング初心者の当時の自分にとっては、実装していて楽しく、記事が豊富で勉強しやすいそんな印象のフレームワークでした。
直近を振り返ると、PythonのWebフレームワークを使うことが多く、長らくRailsに触れてこなかったので、アドベントカレンダーを機にRailsのお作法を思い出しながら、Railsの魅力をほんの少しだけ共有できたらと思います。

Rails公式サイト
https://rubyonrails.org

この記事でやること・やらないこと

やること

この記事では、Ruby on Railsとsqlite(DB)を使って、簡単なCRUD Web APIアプリケーションを作成します。
筆者は当時、部員管理システム(今は運用していない)を作成するためにRailsを勉強したので、今回はより簡易的な部員管理システムを例にRailsの基礎を確認しながら実装していきます。
サーバーの動作確認として、PostmanというHTTPクライアントツールを使用してテストします。

やらないこと

この記事では、各種ツールのインストール方法については言及しません。また、Rubyの構文、SQLについても言及しません。
読んでいて、構文等で不明な点については、他の方が公開されている素晴らしい記事をご参照ください。

実装の前に

Railsとは

Ruby on Rails(以下、Rails)とは、プログラミング言語のRubyによって書かれたwebアプリケーションフレームワークです。
フレームワークとは、日本語にすると「枠組み」などと訳されることが多いです。
この字から連想できるように、フレームワークではよく使われる機能が枠組みとして予め実装されています。そのため、フレームワークの利用者は必要とする機能を取り入れながら目的のシステムを実装することができます。
フレームワークを利用するメリットは、予め組み込まれた機能を使いながら実装できるため、フレームワークを使わずに実装するよりも自分で書くコードを減らすことができる点です。これにより、スピード感のあるシステム開発が可能となります。

MVCについて

Railsの設計思想にMVCという考え方があります。
MVCとは、Model View Controllerの頭文字から成っている言葉です。
Railsで実装していくにあたり、これらの言葉が出てくるので簡潔ではありますが確認します。
Railsの文脈で出るMVCについては、私は以下のように解釈しています。

  • Model・・・DBの定義及びDBの操作に関する部分
  • View・・・ユーザーに表示するコンテンツに関する部分
  • Controller・・・ユーザーのアクセスによって実行する処理に関する部分

この記事では、ModelとControllerを主に扱います。
Railsの文脈ででるMVCというわかりにくい表現をしましたが、いろいろ議論があるようなので詳しくは調べてみてください。

では早速、始めていきましょう。

動作確認済みの環境

  • macOS Sonoma 14.1.1
  • ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
  • rails 7.1.2
  • Postman

前準備

プロジェクト作成

プロジェクトを配置する任意のディレクトリで以下のコマンドを実行し、プロジェクトを作成しましよう。
プロジェクト名は任意の名前で作成することができますが、ここではmember_managementで作成します。
今回は、APIのみ実装するので--apiオプションを付けています。
HTMLを返すWebアプリケーションを実装する際は、このオプションを外してください。

# member_managementというプロジェクトを作成
% rails new member_management --api

作成したプロジェクトに移動すると、自動でappやbinなどのフォルダやファイルが生成されたと思います。

# member_managementディレクトリに移動
% cd mamber_management

ではここで、サーバーを立ち上げてみましょう。

# サーバー起動
% rails s
=> Booting Puma
=> Rails 7.1.2 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 70000
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
ポートを指定して起動する
# 例)8080ポートで起動
% rails s -p 8080

コマンドを実行すると、上記のようなログが表示されます。(環境により異なる可能性があります)
この例では、「http://127.0.0.1:3000」でサーバーが立ち上がっているので、ブラウザにこのURLを打ち込んでみましょう。以下のようなページが立ち上がっていれば成功です。
これは、ご自身のマシン上でサーバーが立ち上がっていて、3000ポートでこのプログラムがリッスンしていることを表しています。
ページが表示されることを確認できたら、「Ctrl + C」で、サーバーを停止します。

テーブル作成

APIを実装していくにあたり、まずはDBの設計からしていきます。
今回は部員情報として、以下の項目を扱いたいです。

  • 氏名(文字列)
  • 学年(整数)
  • 学籍番号(文字列)
  • 兼部しているか(論理型)

これらに加え、プライマリキーとしてidを使用するため、以下のようなテーブルをDBに作成したいです。

id name grade student_num is_multiple
1 Sato 1 abcde true
2 Suzuki 2 fghij false

SQL文を使用してテーブルを作成しても良いのですが、railsにはこんな時に便利なコマンドがあります。
以下のコマンドを実行することで、テーブルを作成するモトになるマイグレーションファイルというものを自動的に生成してくれます。

# マイグレーションファイルの作成
# rails g model モデル名 カラム1:型 カラム2:型 ...
% rails g model User name:string grade:integer student_num:string is_multiple:boolean
指定可能な型
  • integer
  • primary_key
  • decimal
  • float
  • boolean
  • binary
  • string
  • text
  • date
  • time
  • datetime

すると、以下に新たにファイルが生成されます。

  • /app/models/user.rb
  • /db/migrate/yyyymmddhhmmss_create_users.rb
  • /test/models/user_test.rb

ここで、空の値が保存されないように各カラムにNot Null制約を付します。
制約を追加するには、作成したマイグレーションファイルの各カラムに「null: false」を追記すれば良いです。
下記のように編集します。

# /db/migrate/yyyymmddhhmmss_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.integer :grade, null: false
      t.string :student_num, null: false
      t.boolean :is_multiple, null: false

      t.timestamps
    end
  end
end

編集したマイグレーションファイルを実際のDBに反映させましょう。

# マイグレーションファイルを元にテーブル作成
% rails db:migrate

作成したテーブルを確認します。

% rails dbconsole
sqlite> .schema
~
CREATE TABLE IF NOT EXISTS "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "grade" integer, "student_num" varchar, "is_multiple" boolean, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
~

適切にテーブルが作成されていることが確認できました。
最後に、自動で作成されたカラムを含め、作成したテーブルの構造をまとめます。

  • id(integer):プライマリキー
  • name(string):氏名
  • grade(integer):学年
  • student_num(string):学籍番号
  • is_multiple(boolean):兼部の有無
  • created_at(datetime):作成日
  • updated_at(datetime):更新日

コントローラーファイルの作成

次に、具体的な処理部分であるコントローラーを作成していきます。
コントローラーでは、URLに対応する処理を定義します。
例えば、/indexにアクセスが来た時にユーザー一覧を返したり、/updateにアクセスが来た時にリクエストに対応するユーザーの情報を更新したりといった、アクセスごとの振る舞いを定義していきます。
今回実装する機能としては以下の通りです。

  • 部員情報の新規作成(Create)
  • 部員情報の一覧表示(Read)
  • 部員情報の更新(Update)
  • 部員情報の削除(Delete)

これらを参考にコントローラーを作成していきます。
コントローラーファイルを作成するコマンドも用意されているので、下記のコマンドでコントローラーファイルを作成します。

# コントローラーファイルの作成
% rails g controller Users register index update delete

/app/controllers/users_controller.rbが生成されました。
このファイルに処理を書いていきます。

CRUD実装

ユーザー新規登録

新規作成機能では、下記のようなJSONをPOSTメソッドでリクエストしてユーザーを作成する機能を実装します。
受け取るURLは、/users/register/とします。

// POST:/users/register/
{
    "name":"hoge",
    "grade":1,
    "student_num":"abcd",
    "is_multiple":false
}

では、初めに/config/routes.rbを編集し、受け取るURLの設定をします。
赤色のハイライト部分を、緑色のハイライト部分に書き換えます。
getやpostの部分では、HTTPリクエストメソッドを指定しています。
次の'users/register'では、受け取り口のURLを定義しています。
to以降の'users#register'では、定義したURLにアクセスが来た際に、どのコントローラーのどのメソッドを実行するかを定義しています。
今回の場合は、users/registerPOSTでリクエストが来たら、usersコントローラーregisterメソッドを実行するということになります。

 # /config/routes.rb
Rails.application.routes.draw do
- get 'users/register'
+ post 'users/register', to: 'users#register'
  get 'users/index'
  get 'users/update'
  get 'users/delete'
end
to の別表記
# 以下は同じ意味です
post 'users/register', to: 'users#register'
post 'users/register' => 'users#register'

次に、コントローラーのregisterメソッド部分を書いていきます。

# /app/controllers/users_controller.rb
class UsersController < ApplicationController
  def register
    user = User.new(user_params)
    if user.save # ←ここで保存
      render json: { user: user },status: 201
    else
      render status: 400
    end
  end
# ・・・
  private
  def user_params
    params.require(:user).permit(:name, :grade, :student_num, :is_multiple)
  end
end

User.newでは、モデルで作成したUserモデルを元にインスタンスを生成しています。
user_paramsというプライベートメソッドは、ストロングパラメーターというもので、不正な値を保存してしまないよう、指定したパラメーターのみ使用するためのものです。
requireにはモデル名(今回であればuser)、permitにはカラム名を入力します。
user.saveでインスタンスの情報をDBに保存しています。
保存に成功したかどうかで、返すJSONを切り替えています。

参考:
https://ichigick.com/rails-strong-parameter/

動作確認

サーバーを起動したのちPostmanで、サーバー宛に新規登録のリクエストを投げてみましょう。

% rails s

登録するJSONの例は以下のとおりです。

{
    "name":"hoge",
    "grade":1,
    "student_num":"abcd",
    "is_multiple":false
}

ステータス201で、登録した情報が返って来れば成功です!🙌

成功例

次に、あえて名前の部分を消してリクエストしてみます。
すると、エラーが発生したことがわかり、Not Null制約が機能していることがわかります。
本来であれば、エラーハンドリングで適切に処理すべきだと思いますが、この記事では簡略化のため割愛します。

{
    "grade":1,
    "student_num":"abcd",
    "is_multiple":false
}
失敗例

ユーザー一覧取得

このセクションでは、登録してあるユーザー情報の一覧を全て表示する機能を実装します。
具体的には、GETで/users/indexにアクセスがあった時に、以下のような形式のJSONを返す機能です。

// GET:/users/index
{
    "users": [
        {
            "id": 1,
            "name": "hoge",
            "grade": 1,
            "student_num": "abcd",
            "is_multiple": false,
            "created_at": "2023-12-18T13:24:00.501Z",
            "updated_at": "2023-12-18T13:24:00.501Z"
        },
        {
            "id": 2,
            "name": "fuga",
            "grade": 2,
            "student_num": "efgh",
            "is_multiple": true,
            "created_at": "2023-12-19T00:55:03.268Z",
            "updated_at": "2023-12-19T00:55:03.268Z"
        },
	// ・・・
    ]
}

先ほどルーティングファイルを編集したときと同様に、/users/indexGETでアクセスが来た時に、usersコントローラーindexメソッドが実行されるよう設定します。

 # /config/routes.rb
Rails.application.routes.draw do
  post 'users/register', to: 'users#register'
- get 'users/index'
+ get 'users/index', to: 'users#index'
  get 'users/update'
  get 'users/delete'
end

次に、コントローラーに処理を書いていきます。
users内のレコードを全て取得するには、モデルで作成したUserのallメソッドを実行することで取得できます。
取得したレコードが空かどうかをempty?でチェックし、レスポンスを切り替えています。

# /app/controllers/users_controller.rb
class UsersController < ApplicationController
# ・・・
  def index
    users = User.all # ←ここでユーザーテーブルからの全取得
    if users.empty?
      render status: 400
    else
      render json: { users: users },status: 200
    end
  end
# ・・・
end

動作確認

サーバーを起動し直して先ほどと同様に一覧取得のリクエストを投げてみましょう。

% rails s

ステータス200で、ユーザー情報の一覧が返って来れば成功です!🙌

成功例

ユーザー情報更新

ユーザー情報更新では、URLのリクエストによって動的にユーザーの情報を更新する機能を実装します。
PUTで、/users/update/[id] 宛に以下のようなJSONが来た時に、URLに含まれるid部分と同じidを持つレコードがDB内にある時に、JSONの中身でDBを更新するという機能です。
もし、該当レコードがDBにない場合は、新規作成することにします。

ルーティングファイルは、以下のようにします。
見慣れない /:user ですが、これは例えば/users/register/1や/users/register/hogeなどのリクエストが来た際に、今回の例であれば1やhogeの情報を指定先のコントローラーのメソッド内で扱えるようにするためのものです。

 # /config/routes.rb
Rails.application.routes.draw do
  post 'users/register', to: 'users#register'
  get 'users/index', to: 'users#index'
- get 'users/update'
+ put 'users/update/:id', to: 'users#update'
  get 'users/delete'
end

次に、コントローラーのupdateメソッドを書きます。
URLに含まれるid部分は、params[:id]で取得しています。
find_byメソッドは、引数の値に該当するレコードを1つ取り出すメソッドです。
もし、見つからなかった場合は、nilが返ります。

# /app/controllers/users_controller.rb
class UsersController < ApplicationController
# ・・・
  def update
    user = User.find_by(id: params[:id]) # ← ここでプライマリキー(id)を引数のidで検索
    if user.nil?
      # DBにレコードがない場合
      user = User.new(user_params)
      if user.save
        render json: { user: user },status: 201
      else 
        render status: 400
      end
    else
      # DBにレコードがある場合
      if user.update(user_params) # ←ここで更新
        render json: { user: user },status: 200
      else 
        render status: 400
      end
    end
  end
# ・・・
end

動作確認

ユーザーidが1の学年情報を更新して、うまくできているか確認します。
リクエストしたJSONの例は以下のとおりです。

{
    "grade":5
}

ステータス200で、更新後のユーザー情報が返って来れば成功です!🙌

成功例

ユーザー情報削除

ここでは、ユーザー情報を削除する機能を実装します。
DELETEで、/users/delete/[id] 宛にリクエストがあった際に、アクセスURLに含まれるidと同じidを持つユーザーレコードをDBから削除するという機能です。
更新機能と同じ要領で、ルーティングファイルを記述します。
更新機能と大きく違う点は、リクエストメソッドをdeleteとしている点です。

 # /config/routes.rb
Rails.application.routes.draw do
  post 'users/register', to: 'users#register'
  get 'users/index', to: 'users#index'
  put 'users/update/:id', to: 'users#update'
- get 'users/delete'
+ delete 'users/delete/:id', to: 'users#delete'
end

コントローラーのdeleteメソッドは、以下のようにします。
ここで新たに出てきたメソッドは、destroyです。
これは、1つのレコードをDBから削除する際に使用します。

# /app/controllers/users_controller.rb
class UsersController < ApplicationController
# ・・・
  def delete
    user = User.find_by(id: params[:id])
    if user.nil?
      render status: 400
    else
      # DBにレコードがある場合
      if user.destroy # ←ここで削除
        render status: 204
      else
        render status: 400
      end
    end
  end
# ・・・
end

動作確認

最後に、ユーザーidが1のユーザーを削除するリクエストを投げます。
ステータス204が返って来れば成功です!🙌

成功例

最後に

お疲れ様でした〜🍵

この記事では、簡単なCRUD機能を持つWeb APIアプリケーションを作成しました。
今回触れなかった、DBの外部キー参照や、より詳細なバリデーション設定を学ぶことで、実用的なシステムに近づくと思います。

また、今回は直接SQLで操作することはありませんでしたが、興味のある方は別途勉強してみるといいと思います。

この記事を通して、Railsに興味を持っていただければ幸いです。
読みにくい文章だったと思いますが、最後まで読んでいただきありがとうございます。
間違い等ありましたら、連絡いただけると嬉しいです。

サンプルコード

https://github.com/sasan0/member_management_sample_code

参考にさせていただいたサイト

https://guides.rubyonrails.org/getting_started.html
https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9
https://qiita.com/List202/items/b7b526934fa4dd865f2c
https://ichigick.com/rails-strong-parameter/
https://qiita.com/terufumi1122/items/997e24dde87f807e3944
https://developer.mozilla.org/ja/docs/Web/HTTP/Status

Discussion