Building A Tiny Web App

27 November 2015

I’ve been infatuated with the LessCode movement as of late. A few days ago I set out to see how quickly I could construct a small database backed application. Granted it did take a bit longer than 15 minutes, the libraries used in this project are quite small and pretty easy to understand. We’ll avoid magic, and walk through each step.

Libraries used: Cuba, Mote, Nomadize, SQLCapsule

We’ll be building a super simple used car inventory app. Sometimes I like to start with the web interface when building an application, so we’ll setup Cuba and make a few small routes for viewing and adding vehicles to the inventory.

$ mkdir inventory_app && cd inventory_app
$ touch Rakefile Gemfile

Setup the Gemfile:

# Gemfile
source 'https://rubygems.org'

gem 'rake'
gem 'cuba'
gem 'mote'
gem 'mote-render'

Now we can bundle install and get started on defining the routes, we’re going to need to build some views so we’ll go ahead and create those directories and views.

$ bundle install
$ touch app.rb
$ touch config.ru
$ mkdir -p app/views
$ touch app/home.mote app/layout.mote
# app.rb
require 'cuba'
require 'mote'
require 'mote/render'

# setup mote renderer
Cuba.plugin(Mote::Render)

# tell mote where to find its view files
Cuba.settings[:mote][:views] = "./app/views/"

Cuba.define do

  on root do
    # render app/views/home.mote
    res.write(view("home"))
  end

end

config.ru is pretty basic it just launches the app:

# config.ru
require './app'

run(Cuba)

layout.mote is also pretty simple:

<html>
  <head>
     <title>Inventory App</title>
  </head>
  <body>
    {{ content }}
  </body>
</html>

and lastly home is just a simple H1 and a link to create a vehicle.

<h1>Car Inventory</h1>
<a href="vehicles/new">Add Vehicle</a>

At this point you should be able to use $ rackup and actually visit localhost:9292 and see your home template rendered.

The add vehicle route and view don’t exist yet, so we’ll add them:

  on "vehicles/new" do
    res.write(view("vehicles/new"))
  end

The corresponding view:

$ mkdir -b app/views/vehicles
$ touch app/views/vehicles/new.mote

A vehicle should have a ‘make’, ‘model’, ‘mileage’, and ‘color’, so we build a simple form for that:

<form action="/vehicles/create" method="post">
  <input type="text" name="make" value="make">
  <input type="text" name="model" value="model">
  <input type="text" name="mileage" value="mileage">
  <input type="text" name="color" value="color">

  <input type="submit" value="Submit">
</form>

the vehicles/create route 404’s when we click the ‘submit’ button. We should add a post route to our app.rb file:

# ...
  on post do

    on "vehicles/create" do
        on param("make"), param("model"), param("mileage"), param("color") do |make, model, mileage, color|
          puts "make: #{make}, model: #{model}, mileage: #{mileage}, color: #{color}"
          res.redirect "/"
      end
    end

  end
# ...

Now when we fill in some details and click ‘submit’ we get our expected input in the log.

make: Toyota, model: Camry, mileage: 201593, color: Green
127.0.0.1 - - [26/Nov/2015:10:07:04 -0600] "POST /vehicles/create HTTP/1.1" 302 - 0.0018
127.0.0.1 - - [26/Nov/2015:10:07:04 -0600] "GET / HTTP/1.1" 200 152 0.0007

Outputting to the log is cool, but not very useful, we’d really like to persist this so we can view it later.

To do this we’ll have to setup postgres using the Nomadize gem. We’ll update the Gemfile to include Nomadize:

# Gemfile
gem nomadize

then

$ bundle install

and lastly we’ll update our Rakefile to get the Nomadize rake tasks:

# Rakefile
require 'nomadize/tasks'

Now we can use the migration/database utility tasks provided by Nomadize:

$ bundle exec rake -T
rake db:create                         # Create database using name in {appdir}/config/database.yml
rake db:drop                           # drop database using name in {appdir}/config/database.yml
rake db:generate_template_config       # generate template config file
rake db:migrate                        # Run migrations
rake db:new_migration[migration_name]  # Generate a migration template file
rake db:rollback[steps]                # rollback migrations (default count: 1)
rake db:status                         # view the status of known migrations

We’ll generate the template config and update it with our details:

$ bundle exec rake db:generate_template_config
Config created in config/database.yml

Now if we open that file we see basic database config file, let’s update it to include the database names:

# ./config/database.yml
---
development:
  :dbname: 'inventory_app_development'
test:
  :dbname: 'inventory_app_test'
production:
  :dbname: 'inventory_app_production'

Now we can go back to the console and use the rake task to create the database:

$ bundle exec rake db:create
CREATE DATABASE inventory_app_development;

Great, so we have a database, but we still need migrations:

$ bundle exec rake db:new_migration[add_vehicles_table]

This creates a file in ./db/migrations for us to add our migrations:

# ./db/migrations/[timestamp]_add_vehicles_table.yml
---
:up: 'CREATE TABLE vehicles (
        make    VARCHAR(50) NOT NULL,
        model   VARCHAR(50) NOT NULL,
        mileage INTEGER     NOT NULL,
        color   VARCHAR(50) NOT NULL,
        id      SERIAL
        );'
:down: 'DROP TABLE vehicles;'

Run the migration, and check the status:

$ bundle exec rake db:migrate
$ bundle exec rake db:status
filename                          |  status
-------------------------------------------
20151126162330_add_vehicles_table |      up

So we’ve got a table, now let’s put things in it. To do that we’ll use SQLCapsule. Add it to the Gemfile and Bundle.

# Gemfile
# ...
gem 'sql_capsule'
$ bundle install

To hook it together we require nomadize/config and sql_capsule and then use them to actually insert a vehicle into our database:

# app.rb
require 'cuba'
require 'mote'
require 'mote/render'
require 'nomadize/config'
require 'sql_capsule'

Cuba.plugin(Mote::Render)
Cuba.settings[:mote][:views] = "./app/views/"

Cuba.define do

  on root do
    res.write(view("home"))
  end

  on "vehicles/new" do
    res.write(view("vehicles/new"))
  end

  on post do
    on "vehicles/create" do
      on param("make"), param("model"), param("mileage"), param("color") do |make, model, mileage, color|
        db              = Nomadize::Config.db
        vehicle_queries = SQLCapsule.wrap(@db)
        save_query = "INSERT INTO vehicles (make, model, mileage, color) VALUES ($1, $2, $3, $4) RETURNING id;"
        vehicle_queries.register :add_vehicle, save_query, :make, :model, :mileage, :color

        result = vehicle_queries.run :add_vehicle, make: make, model: model, mileage: mileage, color: color
        puts result
        res.redirect "/"
      end
    end
  end

end

It seems like we can add vehicles, but it’d be better if we could list the vehicles on the home page to be sure:

<!-- home.mote -->
<h1>Car Inventory</h1>
<a href="vehicles/new">Add Vehicle</a>
<ul>
% vehicles.each do |vehicle|
  <li>{{ vehicle }}</li>
% end
</ul>

And we’ll need to update the on root handler to pass in the list of vehicles:

# app.rb
# ...
  on root do
    db               = Nomadize::Config.db
    vehicle_queries  = SQLCapsule.wrap(db)

    all_vehicles_sql = "SELECT * FROM vehicles;"
    vehicle_queries.register :all_vehicles, all_vehicles_sql

    vehicles = vehicle_queries.run(:all_vehicles)
    res.write(view("home", vehicles: vehicles))
  end

The root handler now shares a lot of database setup with our vehicles/create handler, so we’ll push it out into a DB class that is responsible for holding our registered queries.

# app/models/db.rb
require 'nomadize/config'
require 'sql_capsule'

class DB
  def self.vehicle_queries
    @vehicle_queries ||= begin
      query_holder = SQLCapsule.wrap(Nomadize::Config.db)
      setup_vehicle_queries(query_holder)
    end
  end

  private

  def self.setup_vehicle_queries(holder)
    all_vehicles_sql = "SELECT * FROM vehicles;"
    holder.register :all_vehicles, all_vehicles_sql

    save_vehicle_sql = "INSERT INTO vehicles (make, model, mileage, color) VALUES ($1, $2, $3, $4) RETURNING id;"
    holder.register :add_vehicle, save_vehicle_sql, :make, :model, :mileage, :color

    holder
  end
end

Now we can access saved vehicle queries by using DB.vehicle_queries and our app ends up looking something like this:

# app.rb
require 'cuba'
require 'mote'
require 'mote/render'

require_relative 'app/models/db'

Cuba.plugin(Mote::Render)
Cuba.settings[:mote][:views] = "./app/views/"

Cuba.define do

  on root do
    # get the vehicles from the database
    vehicles = DB.vehicle_queries.run :all_vehicles

    # pass the vehicles into the template
    res.write(view("home", vehicles: vehicles))
  end

  on "vehicles/new" do
    res.write(view("vehicles/new"))
  end

  on post do
    on "vehicles/create" do
      on param("make"), param("model"), param("mileage"), param("color") do |make, model, mileage, color|
        # save the vehicle
        id = DB.vehicle_queries.run :add_vehicle, make: make, model: model, mileage: mileage, color: color

        # log the id and redirect to root
        puts id
        res.redirect "/"
      end
    end
  end

end

The last thing we need is a way to mark a vehicle as sold. To do this we should add a sell button to the root page along with a field to enter the sold amount.

<!-- home.mote -->
<h1>Car Inventory</h1>
<a href="vehicles/new">Add Vehicle</a>
<ul>
% vehicles.each do |vehicle|
  <li>
    {{ vehicle }}
    <form action="/sales/create" method="post">
      <input type="hidden" name="vehicle_id" value={{ vehicle["id"] }}>
      <input type="text"  name="amount" value="amount">
      <input type="submit" value="Sold!">
    </form>
  </li>
% end
</ul>

We update the app file to support the route and output the params.

# app.rb
# ...
    on "sales/create" do
      on param("vehicle_id"), param("amount") do |vehicle_id, amount|
        puts "vehicle_id: #{vehicle_id}, amount: #{amount}"
        res.redirect "/"
      end
    end
# ...

And see that they come through:

127.0.0.1 - - [27/Nov/2015:11:29:44 -0600] "GET / HTTP/1.1" 200 2081 0.0050
vehicle_id: 3, amount: 2500
127.0.0.1 - - [27/Nov/2015:11:29:48 -0600] "POST /sales/create HTTP/1.1" 302 - 0.0023
127.0.0.1 - - [27/Nov/2015:11:29:48 -0600] "GET / HTTP/1.1" 200 2081 0.0007

Now we need a sales table:

$ bundle exec rake db:add_migration[add_sales_table]

And fill in the details:

# ./db/migrations/[timestamp]_add_sales_table.yml
---
:up: 'CREATE TABLE sales (
       id         SERIAL,
       amount     INTEGER NOT NULL,
       vehicle_id INTEGER NOT NULL
       );'
:down: 'DROP TABLE sales;'

And migrate:

$ bundle exec rake db:migrate

We’ll update our DB class, and add another class level method/instance variable for sales queries:

# /app/models/db.rb
# ...
  def self.sales_queries
    @sales_queries ||= begin
      query_holder = SQLCapsule.wrap(Nomadize::Config.db)
      setup_sales_queries(query_holder)
    end
  end
# ...
  private

  def self.setup_sales_queries(holder)
    save_sale_sql = "INSERT INTO sales (vehicle_id, amount) VALUES ($1, $2) RETURNING id;"
    holder.register :add_sale, save_sale_sql, :vehicle_id, :amount

    holder
  end
# ...

Now we can hook it together:

# app.rb
# ...
    on "sales/create" do
      on param("vehicle_id"), param("amount") do |vehicle_id, amount|
        id = DB.sales_queries.run :add_sale, vehicle_id: vehicle_id, amount: amount
        puts id
        res.redirect "/"
      end
    end
# ...

This creates sales, but our root index page still shows all the vehicles (even the sold ones), that seems bad. Let’s add another vehicle query that gets just the available vehicles (that is the ones that aren’t in the sales table)

# app/models/db.rb
# ...
def self.setup_vehicle_queries(holder)
    # ...
    available_vehicles_sql = "SELECT * FROM vehicles LEFT JOIN sales ON vehicles.id=sales.vehicle_id WHERE sales.vehicle_id IS NULL;"
    holder.register :available_vehicles, available_vehicles_sql

    holder
end
# ...

We update the root handler to use available_vehicles instead:

# app.rb
# ...
  on root do
    vehicles = DB.vehicle_queries.run :available_vehicles
    res.write(view("home", vehicles: vehicles))
  end
# ...

Now when we restart the app we get… an ERROR OMG:

[2015-11-27 12:00:49] INFO  WEBrick::HTTPServer#start: pid=3051 port=9292
SQLCapsule::Wrapper::DuplicateColumnNamesError: Error duplicate column names in resulting table: ["make", "model", "mileage", "color", "id", "id", "amount", "vehicle_id"]
This usually happens when using a `JOIN` with a `SELECT *`
You may need use `AS` to name your columns.
QUERY: SELECT * FROM vehicles LEFT JOIN sales ON vehicles.id=sales.vehicle_id WHERE sales.vehicle_id IS NULL;

So it looks like our join is returning a bunch of columns we don’t really care about: ["make", "model", "mileage", "color", "id", "id", "amount", "vehicle_id"] let’s update the query to just grab the vehicle information.

# app/models/db.rb
# ...
  def self.setup_vehicle_queries(holder)
  # ...
    available_vehicles_sql = "SELECT vehicles.make, vehicles.model, vehicles.mileage, vehicles.color, vehicles.id FROM vehicles LEFT JOIN sales ON vehicles.id=sales.vehicle_id WHERE sales.vehicle_id IS NULL;"
    holder.register :available_vehicles, available_vehicles_sql

    holder
  end
# ...

Now our page loads again, and we see only the available vehicles. We can mark vehicles as sold and they are removed from the list.

It would be great if now we could display the total sales, or a list of the sold vehicles etc, but since this post is already eye glazingly long I’ll leave that as an exercise for the viewer. ;-)

Notes: I’m not totally sold on my method of pushing the database setup into class methods on the DB class. If we were building an actual app I think that would be difficult to test and the setup_vehicle_queries method would likely get unruly over time. However, for the purposes of this post/experiment I think it works alright.

Hopefully you now have a better idea of how easy it can be to build a database backed web application using a minimal stack.

Our resulting project looks like:

.
├── app
│   ├── models
│   │   └── db.rb
│   └── views
│       ├── home.mote
│       ├── layout.mote
│       └── vehicles
│           └── new.mote
├── app.rb
├── config
│   └── database.yml
├── config.ru
├── db
│   └── migrations
│       ├── 20151126162330_add_vehicles_table.yml
│       └── 20151127173600_add_sales_table.yml
├── Gemfile
├── Gemfile.lock
└── Rakefile