This tutorial will go over building a fully functional CRUD (Create, Read, Update, Delete) application with Sinatra. Following POODR tradition our application will be a bicycle related app. The application will allow a user to create a bike project, i.e. Mountain Bike, set a budget, and add a list of parts with prices that will be needed to complete the project. With this project we will be able to demonstrate how to implement each CRUD action within the Sinatra framework. The complete project repo is available here: Sinatra Bike Project.

To begin we will set up our migrations and associations. Our app will have users, projects, and parts tables. Users will have many projects and have many parts through projects. Projects will have many parts and belong to a user. And finally Parts will belong to a project. If you are familiar will Rails generators, Sinatra has a similar way of generating migrations with the help of rake. You can view all available rake commands with rake -T. To create the migrations we can use the rake db:create_migration NAME=create_table where table is the table name. Once we have the migrations we can run rake db:migrate. Next we can create our three models with the appropriate ActiveRecord associations.

user.rb

1
2
3
4
5
6
class User < ActiveRecord::Base
  validates_presence_of :username, :email
  has_secure_password
  has_many :projects
  has_many :parts, through: :projects
end

project.rb

1
2
3
4
5
class Project < ActiveRecord::Base
  validates_presence_of :title, :budget
  belongs_to :user
  has_many :parts
end

part.rb

1
2
3
4
class Part < ActiveRecord::Base
  validates_presence_of :name, :price
  belongs_to :project
end

Once we have our models and associations set up we can set up our four controllers: ApplicationController, UsersController, ProjectsController and PartsController. These files will handle all of the routes and HTTP actions a user will need to maintain a specific project. They will also handle validations and redirect with appropriate error messages.

application_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
require './config/environment'

class ApplicationController < Sinatra::Base

  configure do
    set :public_folder, 'public'
    set :views, 'app/views'
    enable :sessions
    set :session_secret, "bike_security"
  end

  get '/' do
    erb :welcome
  end

  helpers do
    def logged_in?
      !!session[:user_id]
    end

    def current_user
      User.find(session[:user_id])
    end
  end

end

users_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class UsersController < ApplicationController
  get '/signup' do
    if !logged_in?
      erb :'users/signup'
    else
      redirect to '/projects'
    end
  end

  post '/signup' do
    if params.values.any? {|value| value == ""}
      erb :'users/signup', locals: {message: "You can't do that!"}
    else
      @user = User.new(username: params[:username], email: params[:email], password: params[:password])
      @user.save
      session[:user_id] = @user.id
      redirect to '/projects'
    end
  end

  get '/login' do
    if !logged_in?
      erb :'users/login'
    else
      redirect to '/projects'
    end
  end

  post '/login' do
    user = User.find_by(:username => params[:username])
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect to '/projects'
    else
      erb :'users/login', locals: {message: "You credentials are incorrect!"}
    end
  end

  get '/logout' do
    if session[:user_id] != nil
      session.destroy
      redirect to '/'
    else
      redirect to '/projects'
    end
  end

end

projects_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class ProjectsController < ApplicationController
  get '/projects' do
    if logged_in?
      @projects = Project.all
      erb :'projects/index'
    else
      erb :'users/login', locals: {message: "You don't have access, please login"} 
    end
  end

  get '/projects/new' do
    if logged_in?
      erb :'projects/new'
    else
      erb :'users/login', locals: {message: "You don't have access, please login"}
    end
  end

  post '/projects' do
    if params.values.any? {|value| value == ""}
      erb :'projects/new', locals: {message: "Your missing information!"}
    else
      user = User.find(session[:user_id])
      @project = Project.create(title: params[:title], budget: params[:budget], user_id: user.id)
      redirect to "/projects/#{@project.id}"
    end
  end

  get '/projects/:id' do 
    if logged_in?
      @project = Project.find(params[:id])
      erb :'projects/show'
    else 
      erb :'users/login', locals: {message: "You don't have access, please login"}
    end
  end

  get '/projects/:id/edit' do
    if logged_in?
      @project = Project.find(params[:id])
      if @project.user_id == session[:user_id]
       erb :'projects/edit'
      else
      erb :'projects', locals: {message: "You don't have access to edit this project"}
      end
    else
      erb :'users/login', locals: {message: "You don't have access, please login"}
    end
  end

  patch '/projects/:id' do 
    if params.values.any? {|value| value == ""}
      redirect to "/projects/#{params[:id]}/edit"
    else
      @project = Project.find(params[:id])
      @project.title = params[:title]
      @project.budget = params[:budget]
      @project.save
      redirect to "/projects/#{@project.id}"
    end
  end

  delete '/projects/:id/delete' do 
    @project = Project.find(params[:id])
    if session[:user_id]
      @project = Project.find(params[:id])
      if @project.user_id == session[:user_id]
        @project.delete
        redirect to '/projects'
      else
        redirect to '/projects'
      end
    else
      redirect to '/login'
    end
  end

end

parts_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class PartsController < ApplicationController
  get '/projects/:id/parts/new' do
    if logged_in?
      @project = Project.find(params[:id])
      erb :'parts/new'
    else
      erb :'users/login', locals: {message: "You don't have access, please login"}
    end
  end

  post '/projects/:id' do
    if params.values.any? {|value| value == ""}
      @project = Project.find(params[:id])
      erb :'parts/new', locals: {message: "You are missing information!"}
    else
      @project = Project.find(params[:id])
      @part = Part.new(name: params[:name], price: params[:price])
      @part.save
      @project.parts << @part
      redirect to "/projects/#{@project.id}"
    end
  end

  delete '/projects/:id/parts/:part_id/delete' do 
    @project = Project.find(params[:id])
    @part = Part.find(params[:part_id])
    if logged_in?
      @project = Project.find(params[:id])
      if @project.user_id == session[:user_id]
        @part = Part.find(params[:part_id])
        @part.delete
        redirect to "/projects/#{@project.id}"
      else
        redirect to "/projects/#{@project.id}"
      end
    else
      erb :'users/login', locals: {message: "You don't have access, please login"}
    end
  end

end

With all of our logic and controller actions in place all we need to complete the application is create view pages for each get request action.