Sinatra を試してみる

Ruby 製のウェブフレームワークというと Rails が有名だが、もっとミニマムに始められるモノはないかというと、Sinatra というフレームワークがある。Python の Flask や Node.js の Express.js などに似たような感じで使える。

今回は Sinatra のサンプルプロジェクトを作りつつ、RubyGems 周りの概念の整理を、Node.js・npm と比較しながら進めていく。また、作成したファイルは全て以下の GitHub リポジトリに格納している。

目次

Node.js・npm のコマンド整理

まずは Node.js・npm に慣れている人達にとっては当たり前なコマンド類を列挙する。

# Node.js 本体のバージョン管理ツール・ココでは NodeBrew を用いている
$ nodebrew
nodebrew 1.2.0

# システムが利用している Node.js のバージョンを確認する
$ nodebrew list
v18.12.1

current: v18.12.1

# Node.js 本体のバージョン確認
$ node -v
v18.12.1

# パッケージ管理ツールである npm のバージョン確認
$ npm -v
8.19.2

# グローバルインストールされる npm パッケージは以下に格納される
$ npm ls -g
/Users/Neo/.nodebrew/node/v18.12.1/lib

Ruby におけるコマンドの対比

続いて、Ruby における各種コマンドを対比して見てみる。

# Ruby 本体のバージョン管理ツール
$ rbenv -v
rbenv 1.2.0

# システムが利用している Ruby のバージョンを確認する
$ rbenv version
3.2.0 (set by /Users/Neo/.rbenv/version)

# Node.js 本体相当
$ ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-darwin21]

# npm 相当
$ gem -v
3.4.1

# グローバルインストールされる RubyGems の置き場・この下の gems/ 配下にある
$ gem environment gemdir
/Users/Neo/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0
$ gem list

# Bundler : npm における「ローカルインストール」相当
# Bundler 自体は gem コマンドでグローバルインストールする
$ gem install bundler
# どちらのコマンドも同じモノ
$ bundle -v
Bundler version 2.4.13
$ bundler -v
Bundler version 2.4.13

npm における「ローカルインストール」相当の機能は、gem コマンドではなく Bundler という RubyGems が担う。Bundler 自体は gem コマンドでグローバルインストールしておく。bundlebundler、どちらも同じコマンドのエイリアスとして動いている。

Sinatra 用のプロジェクトディレクトリを作る

次はプロジェクトのディレクトリを作ってみる。

$ mkdir practice-sinatra && cd $_

# npm init 相当・Gemfile を作る
$ bundle init
Writing new Gemfile to /Users/Neo/Documents/Dev/Sandboxes/practice-sinatra/Gemfile

# npm install --save 相当・Gemfile が更新され Gemfile.lock が生成される
$ bundle add sinatra

# npm install 相当・Gemfile と Gemfile.lock を基に必要な RubyGems がインストールされる
$ bundle install

bundle (bundler) コマンドを用いて、npm initnpm install 相当のことを行っている。前述のとおり、bundle install コマンドはデフォルトではグローバルな RubyGems と同じ場所にモジュール類を格納しているので、プロジェクトディレクトリ配下に .gitignore などを書かなくても良い状態。もしパスを自分で設定している場合は、vendor/bundle/ ディレクトリを .gitignore に入れると良いだろう。

# frozen_string_literal: true

source "https://rubygems.org"

gem "sinatra", "~> 3.0"

Puma をインストールする

簡単な Sinatra のコードを書いて実行してみたのだが、エラーが出てしまった。その原因と対策を記す。

アプリケーションサーバを何もインストールしていない場合、$ bundle exec rackup コマンドなどを叩くと Couldn't find handler for: puma, thin, falcon, webrick. というエラーが出る。コレは以下の記事が詳しく、Ruby v3 系では WEBrick が標準ライブラリから削除されたため、自分で何らかのアプリケーションサーバ用のモジュールをインストールする必要があるということである。

Sinatra の公式ガイドでは puma が推奨されているので、Puma をインストールする (= Gemfile に記述してやる)。

# Sinatra 推奨のアプリケーションサーバ Puma を入れる
$ bundle add puma

# Gemfile には以下が追記された
# gem "puma", "~> 6.3"

example-1 : クラシックスタイル・単一 Ruby ファイルで起動するシンプル構成

Sinatra のルーティング・コントローラ部分は大きく2種類の書き方がある。最もシンプルなのは「クラシックスタイル」というモノ。

require 'bundler/setup'  # Bundler 使用時にパスを調整するための require
require 'sinatra'  # ← この部分、`Bundler.require` で一括 require もできるが、ココでは明示的に require する

# いきなり get や post を書けるのがクラシックスタイル
get '/' do
  # Ruby の場合コレで文字列を return できている。`/` にアクセスすると以下の文字列がレスポンスされる
  'Hello World! Example 1'
end

このようなコードを書いて、$ bundle exec ruby app.rb で起動できる。

Bundler.require で一括ロードできるのは便利そうではあるが、依存モジュールがパッと見で分かりにくいと感じたので、Node.js 脳な人間は都度 require を書いてやるのが良さそう。w

example-2 : モジュールスタイル・Rack を使って起動する

Express.js でいうミドルウェアをファイル分割したい場合に、方法がいくつかある。

Sinatra.register を使う方法と、Rack::URLMap を使う方法もあるが、一番明示的なのは use でミドルウェアを登録する方法だと思いコレを採用した。

require 'bundler/setup'
require 'sinatra/base'  # example-1 とはロードしているモジュールが違う

# モジュール (モジュラー) スタイル
class App < Sinatra::Base
  get '/' do
    'Hello World! Example 2'
  end
end
class UsersController < Sinatra::Base
  get '/users' do
    'Users'
  end
end
# ルートコントローラ
require './app'

# 必要なコントローラを require → use で登録する
require './controllers/users'

use UsersController

# ルートコントローラを起動する
run App

というワケで、class 化して書けるのがモジュールスタイル。起動には Rack 用の config.ru ファイルを書くので、コレが実質的なエントリポイントとなる。

app.rbrequire の解決とルートパスの定義だけにしておき、配下の各種パスは controllers/ ディレクトリ配下に分割したモジュールたちに任せる、というワケ。コレは Node.js における Express.js や NestJS っぽい感じだと思う。

今回はココまで

Sinatra は ERB にもデフォルトで対応しているようなのでビュー層も作れるし、JSON を返すようなサーバも構築しやすいだろう。ActiveRecord との連携も可能なようなので、DB を繋いで Rails っぽい使い方に発展させていくのも容易だ。