この記事では、RubyのWebアプリケーションフレームワークであるSinatraを使って、シンプルなメモアプリを作成します。
データの保存先にはDBを使わず、JSONファイルを使用します。
1. 実行環境
- macOS:12.5
- Ruby:3.1.0
- Bundler:2.3.12
- sinatra:2.2.2
- webrick:1.7.0
- sinatra-contrib:2.2.2
2. メモアプリの要件
以下のメモアプリを作成します。

- 機能
- メモの作成
- メモの編集
- メモの削除
- メモ一覧の表示
- 特定のメモの表示
3. 作成手順
以下の手順で進めていきます。
- 設計
- 実装
- 改良
3-1. 設計
まずはアプリケーションの設計を行います。
3-1-1. URI設計
RESTfulなURI設計として、今回は以下のURIを使用します。
| Method | Path | Description |
|---|---|---|
| GET | /memos | メモ一覧を表示 |
| GET | /memos/new | メモ作成画面を表示 |
| GET | /memos/{memo_id}/edit | メモ編集画面を表示 |
| POST | /memos | メモを作成 |
| GET | /memos/{memo_id} | 特定のメモを表示 |
| PATCH | /memos/{memo_id} | 特定のメモを編集 |
| DELETE | /memos/{memo_id} | 特定のメモを削除 |
3-1-2. ディレクトリ設計
ディレクトリ構成は以下のようになります。
memo-app/
- public/
- memos.json # データ保存先
- views/
- edit.erb # メモ編集画面
- index.erb # メモ一覧表示画面
- layout.erb # 各ビューの共通部分
- new.erb # メモ作成画面
- show.erb # 特定のメモ表示画面
- memo.rb # ルーティング定義
3-2. 実装
次にメインの機能の実装を行います。
3-2-1. ライブラリの用意
まずは必要なgemのインストールを行います。
以下のコマンドを実行し、作業用ディレクトリにGemfileを作成します。
$ mkdir memo-app
$ cd memo-app
$ bundle init
Gemfileを以下のように編集します。
# frozen_string_literal: true
source "https://rubygems.org"
# gem "rails"
gem "sinatra"
gem "webrick"
gem "sinatra-contrib"
以下のコマンドを実行してgemをインストールします。
$ bundle install
3-2-2. メモ一覧の表示
次にメモ一覧の表示を行います。
まずはメモのデータとして以下のpublic/memos.jsonを作成します。
{
"1": {
"title": "メモ1",
"content": "メモ1の内容"
},
"2": {
"title": "メモ2",
"content": "メモ2の内容"
}
}
- Rubyのハッシュに変換→
{"1"=>{"title"=>"メモ1", "content"=>"メモ1の内容"}, "2"=>{"title"=>"メモ2", "content"=>"メモ2の内容"}}
次にルーティングを定義します。
以下のmemo.rbを作成します。
# frozen_string_literal: true
require 'sinatra'
require 'sinatra/reloader'
require 'json'
FILE_PATH = 'public/memos.json'
def get_memos(file_path)
File.open(file_path) { |f| JSON.parse(f.read) }
end
get '/' do
redirect '/memos'
end
get '/memos' do
@memos = get_memos(FILE_PATH)
erb :index
end
次にメモ一覧を表示するビューを作成します。
以下のviews/layout.erbを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>メモアプリ</title>
</head>
<body>
<div class="container">
<h1 class="mb-4">メモアプリ</h1>
<%= yield %>
</div>
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>
<%= yield %>の部分にそれぞれのビューの内容が埋め込まれます。- CSSにBootstrapを使用しています。
次にメモ一覧を表示するviews/index.erbを作成します。
<ul>
<% @memos.each do |id, post| %>
<li>
<%= post["title"] %>
</li>
<% end %>
</ul>
3-2-3. 特定のメモの表示
次に特定のメモを1件表示させます。
memo.rbに以下のルーティングを追加します。
# get '/memos' do
# ~
get '/memos/:id' do
memos = get_memos(FILE_PATH)
@title = memos[params[:id]]['title']
@content = memos[params[:id]]['content']
erb :show
end
views/show.erbを作成します。
<div>
<p><%= @title %></p>
<p><%= @content %></p>
</div>
<div>
<a class="btn btn-secondary" href="/memos">メモ一覧に戻る</a>
</div>
views/index.erbにメモへのリンクを追加します。
<ul>
<% @memos.each do |id, post| %>
<li>
<a href="/memos/<%= id %>"><%= post["title"] %></a>
</li>
<% end %>
</ul>
3-2-4. メモの作成
次にメモの作成機能を実装します。
memo.rbに以下のルーティングを追加します。
# get '/memos' do
# ~
get '/memos/new' do
erb :new
end
# get '/memos/:id' do
# ~
get '/memos/new'をget '/memos/:id'より上に書くこと。:idはあらゆるURLにマッチするパラメータのため、/memos/:idは/memos/1や/memos/2だけでなく/memos/newにもマッチする。ルーティングは上から順に合致するURLを探すため、/memos/:idが/memos/newより上に定義されていると、/memos/newのgetリクエストを受け取った時にget '/memos/:id'のルーティングにマッチしてしまいエラーとなる
views/new.erbを作成します。
<form action="/memos" method="post">
<div class="mb-3">
<label for="title" class="form-label">タイトル</label>
<input type="text" id="title" name="title" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">内容</label>
<textarea id="content" name="content" class="form-control"></textarea>
</div>
<div>
<input type="submit" value="保存" class="btn btn-primary">
</div>
</form>
memo.rbに以下のルーティングを追加します。
# def get_memos(file_path)
# ~
def set_memos(file_path, memos)
File.open(file_path, 'w') { |f| JSON.dump(memos, f) }
end
# ~
# get '/memos/:id' do
# ~
post '/memos' do
title = params[:title]
content = params[:content]
memos = get_memos(FILE_PATH)
id = (memos.keys.map(&:to_i).max + 1).to_s
memos[id] = { 'title' => title, 'content' => content }
set_memos(FILE_PATH, memos)
redirect '/memos'
end
(memos.keys.map(&:to_i).max + 1).to_sでidの最大値を探してインクリメント- PRGパターンで実装(Post→Redirect→Get)
views/index.erbにメモ作成画面へのリンクを追加します。
<div class="mb-4">
<a class="btn btn-secondary" href="/memos/new">メモを作成</a>
</div>
<ul>
<% @memos.each do |id, post| %>
<li>
<a href="/memos/<%= id %>"><%= post["title"] %></a>
</li>
<% end %>
</ul>
3-2-5. メモの編集
次にメモの編集機能を実装します。
memo.rbに以下のルーティングを追加します。
# post '/memos' do
# ~
get '/memos/:id/edit' do
memos = get_memos(FILE_PATH)
@title = memos[params[:id]]['title']
@content = memos[params[:id]]['content']
erb :edit
end
views/edit.erbを作成します。
<form action="/memos/<%= params[:id] %>" method="post">
<div class="mb-3">
<label for="title" class="form-label">タイトル</label>
<input type="text" id="title" name="title" value="<%= @title %>" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">内容</label>
<textarea id="content" name="content" class="form-control"><%= @content %></textarea>
</div>
<div>
<input type="hidden" name="_method" value="patch">
<input type="submit" value="保存" class="btn btn-primary">
</div>
</form>
- フォームの隠しパラメータ(
hidden)に_methodパラメータで本来送りたいメソッドを指定(value="patch")
memo.rbに以下のルーティングを追加します。
# get '/memos/:id/edit' do
# ~
patch '/memos/:id' do
title = params[:title]
content = params[:content]
memos = get_memos(FILE_PATH)
memos[params[:id]] = { 'title' => title, 'content' => content }
set_memos(FILE_PATH, memos)
redirect "/memos/#{params[:id]}"
end
views/show.erbに編集ボタンを追加します。
<div>
<p><%= @title %></p>
<p><%= @content %></p>
</div>
<div class="d-flex mb-4">
<form action="/memos/<%= params[:id] %>/edit" method="get">
<input type="submit" value="編集" class="btn btn-primary me-2">
</form>
</div>
<div>
<a class="btn btn-secondary" href="/memos">メモ一覧に戻る</a>
</div>
3-2-6. メモの削除
最後にメモの削除機能を実装します。
memo.rbに以下のルーティングを追加します。
# patch '/memos/:id' do
# ~
delete '/memos/:id' do
memos = get_memos(FILE_PATH)
memos.delete(params[:id])
set_memos(FILE_PATH, memos)
redirect '/memos'
end
views/show.erbに削除ボタンを追加します。
<div>
<p><%= @title %></p>
<p><%= @content %></p>
</div>
<div class="d-flex mb-4">
<form action="/memos/<%= params[:id] %>/edit" method="get">
<input type="submit" value="編集" class="btn btn-primary me-2">
</form>
<form action="/memos/<%= params[:id] %>" method="post">
<input type="hidden" name="_method" value="delete">
<input type="submit" value="削除" class="btn btn-primary">
</form>
</div>
<div>
<a class="btn btn-secondary" href="/memos">メモ一覧に戻る</a>
</div>
3-3. 改良
最後にセキュリティ面などの改良を行います。
3-3-1. XSS対策(セキュリティ)
XSS対策として、メモの書き込み内容を表示する部分にエスケープ処理を追加します。
memo.rbに以下を追加します。
require 'cgi'
views/index.erbを以下のように編集します。
<div class="mb-4">
<a class="btn btn-secondary" href="/memos/new">メモを作成</a>
</div>
<ul>
<% @memos.each do |id, post| %>
<li>
<a href="/memos/<%= id %>"><%= CGI.escapeHTML(post["title"]) %></a>
</li>
<% end %>
</ul>
views/show.erbを以下のように編集します。
<div>
<p><%= CGI.escapeHTML(@title) %></p>
<p><%= CGI.escapeHTML(@content) %></p>
</div>
<div class="d-flex mb-4">
<form action="/memos/<%= params[:id] %>/edit" method="get">
<input type="submit" value="編集" class="btn btn-primary me-2">
</form>
<form action="/memos/<%= params[:id] %>" method="post">
<input type="hidden" name="_method" value="delete">
<input type="submit" value="削除" class="btn btn-primary">
</form>
</div>
<div>
<a class="btn btn-secondary" href="/memos">メモ一覧に戻る</a>
</div>
3-3-2. 改行を反映
メモの書き込み内容の改行を反映するように修正します。
views/show.erbを以下のように編集します。
<div>
<p><%= CGI.escapeHTML(@title) %></p>
<p><%= CGI.escapeHTML(@content).gsub(/\n/, '<br/>') %></p>
</div>
<div class="d-flex mb-4">
<form action="/memos/<%= params[:id] %>/edit" method="get">
<input type="submit" value="編集" class="btn btn-primary me-2">
</form>
<form action="/memos/<%= params[:id] %>" method="post">
<input type="hidden" name="_method" value="delete">
<input type="submit" value="削除" class="btn btn-primary">
</form>
</div>
<div>
<a class="btn btn-secondary" href="/memos">メモ一覧に戻る</a>
</div>
以上で終了です。
以下の記事では、今回作成したメモアプリのデータ保存先をPostgreSQLに変更する手順を紹介しています。
【Ruby】Sinatraでメモアプリを作る(DB編) - あまブログ
【参考】