DEV Community

Ethan Gustafson
Ethan Gustafson

Posted on • Edited on

API Creation With Sinatra

Lets generate a new Sinatra application. It’s convenient to use the Corneal Gem, which will generate a new Sinatra application like how Rails can. You can find it here:

NOTE: I am currently using a mac. If you would like to look at the code for the project created in this blog post, you can find it here: https://github.com/GoodGuyGuf/example_users_application

Corneal Gem App Generation:

  1. Open you terminal and Install the gem with: gem install corneal
  2. Choose a place in your computer to generate a new Sinatra application
  3. Generate the application with corneal new APP_NAME which just for this post I will create a simple example application that only keeps track of users information. I will generate my application with corneal new USERS_APPLICATION
  4. Once everything is created, cd into your project directory and run bundle install
  5. I am using visual studio code, and from the terminal can open VSC with running code . in the terminal.

This application is an API, we will not be using views/erb templates. Delete the views folder.

  • In ApplicationController, remove the code for configuration and the get method corneal generated for you.
  • We don't need the public folder, so we can delete it.
  • For this application, we will be using sqlite3 for our database.
  • Corneal already generates the connection to ActiveRecord and sqlite3 by default.

Now lets create our database.

  • If you run rake --tasks in the terminal, you will most likely get a bigdecimal error.
  • If you include gem 'bigdecimal', '1.4.2' in your gemfile, the application will work but it will still give you a warning that bigdecimal is deprecated. Running rake --tasks now works.
  • If you want the warning to go away, run rake --tasks again. When the deprecated message pops up for bigdecimal, cmd + click and follow the route it gives you. This was the warning it gave me:
  • /Users/user/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-4.2.11.3/lib/active_support/core_ext/object/duplicable.rb:111: warning: BigDecimal.new is deprecated; use BigDecimal() method instead.
  • This will take you to a file called duplicable.rb. You'll see this code:
require 'bigdecimal'
class BigDecimal
  # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead
  # raises TypeError exception. Checking here on the runtime whether BigDecimal
  # will allow dup or not.
  begin
    BigDecimal.new('4.56').dup

    def duplicable?
      true
    end
  rescue TypeError
    # can't dup, so use superclass implementation
  end
end
Enter fullscreen mode Exit fullscreen mode

All you have to simply do is change: BigDecimal.new('4.56').dup to BigDecimal('4.56').dup

require 'bigdecimal'
class BigDecimal
  # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead
  # raises TypeError exception. Checking here on the runtime whether BigDecimal
  # will allow dup or not.
  begin
    BigDecimal('4.56').dup

    def duplicable?
      true
    end
  rescue TypeError
    # can't dup, so use superclass implementation
  end
end
Enter fullscreen mode Exit fullscreen mode

Now you don't have to include the bigdecimal gem in your Gemfile.

Remove the gem and run bundle install. The application now works with no bigdecimal warning.

Why does this happen? The corneal gem generated the activerecord gem for you which is an older version. The current version at the time this was written is 6.0, so you can change the version in your gemfile to:

gem 'activerecord', '~> 6.0', '>= 6.0.3.2', :require => 'active_record'
Enter fullscreen mode Exit fullscreen mode

Now you won't have to change the duplicable file. Bigdecimal deprecated warning will go away as well. Update sqlite3 in the gemfile if it is an older version.

Lets create the migration for the users table.

There is a rake tasks command we can use to generate a new migration table.

rake db:create_migration NAME=create_users
Enter fullscreen mode Exit fullscreen mode

Now we have a new empty migration table.

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
  end
end
Enter fullscreen mode Exit fullscreen mode

Lets add some user information.

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :username
      t.string :password_digest

      t.timestamps
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

password_digest is used with the gem bcrypt in order to protect passwords. There is more information on the gem bcrypt here at:

codahale/bcrypt-ruby

What about timestamps? timestamps will give you two columns: created_at & updated_at. Here is a link for more on migration timestamps:

Active Record Migrations - Ruby on Rails Guides

Gem faker is an awesome gem that is used to add many users into our database. I'll use it in this project just to show you how it works.

In our Gemfile, lets change the section where it groups test together to this:

group :development do
  gem 'pry'
  gem 'tux'
  gem 'sqlite3'
  gem 'faker'
end
Enter fullscreen mode Exit fullscreen mode

Why do we group Gems together?

Certain Gems will only be used in certain modes. For example, the gems we list in our development group, will not be installed on the server when the application is deployed. When in production mode, the other gems will not be used if grouped in another development mode.

Awesome. Now lets create our database with rake db:create

There is an issue. ActiveRecord::AdapterNotSpecified: 'development' database is not configured. Available: []

What do we need? A database.yml file.

How to set up Sinatra with ActiveRecord

Add this section to your database.yml file, which should be placed in your config folder:

default: &default
  adapter: sqlite3
  pool: 5
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

test:
  <<: *default
  database: db/test.sqlite3

production:
  adapter: postgresql
  encoding: unicode
  pool: 5
  host: <%= ENV['DATABASE_HOST'] || 'db' %>
  database: <%= ENV['DATABASE_NAME'] || 'sinatra' %>
  username: <%= ENV['DATABASE_USER'] || 'sinatra' %>
  password: <%= ENV['DATABASE_PASSWORD'] || 'sinatra' %>
Enter fullscreen mode Exit fullscreen mode

See how the database has a .sqlite3 ending? Make sure your environment.rb and database.yml file specify the same ending file name for sqlite3. Otherwise the rake console will show that it did not establish a connection to your database to query the users table.

  • run rake db:create to create our database
  • run rake db:migrate to migrate your table to the database
  • Create a seeds.rb file in your db directory
  • Use Gem faker in seeds.rb to create some fake users like so:
5.times do
    User.create(
        name: Faker::Name.name,
        username: Faker::Internet.username,
        password: Faker::Internet.password
    )
end
Enter fullscreen mode Exit fullscreen mode

Make sure to add has_secure_password in the user model in order for this user creation to work. It won't recognize the password column without it.

run rake db:seed to seed the data into the database. To test if this has worked, go into your rakefile and create a new task called rake console:

desc "A console"
task :console do
  Pry.start
end
Enter fullscreen mode Exit fullscreen mode

Run rake console in the terminal and run User.all and you should have 5 users.

Now we will turn this application into a backend API.

NOTE: From this point forward you might have to look at the source code of each gem if you run into troubleshooting issues. "When in doubt, look at the source code."

To do this we will be adding on some gems in order for the application to work as an API.

gem "sinatra-cross_origin"
gem 'rack-contrib'
gem 'fast_jsonapi'
gem 'sinatra-contrib', require: false
Enter fullscreen mode Exit fullscreen mode

These gems will allow us to use Sinatra as an API.

What do these gems do?

sinatra-cross_origin

gem "sinatra-cross_origin"
Enter fullscreen mode Exit fullscreen mode

britg/sinatra-cross_origin

This gem enables CORS. IT allows an origin(domain) to make requests to your Sinatra API.

Since we are using a modular application, we have to register the cross origin. Add this code in your application controller:

register Sinatra::CrossOrigin
Enter fullscreen mode Exit fullscreen mode

What is a modular application? It is one that inherits from Sinatra::Base. This is what the Sinatra documentation states:

"Modular applications must include any desired extensions explicitly by calling register ExtensionModule within the application’s class scope."

The rest of your code should look like this and we'll break it down:

require './config/environment'
require 'sinatra/base' # Your file should require sinatra/base instead of sinatra; otherwise, all of Sinatra’s DSL methods are imported into the main namespace
require 'sinatra/json'

class ApplicationController < Sinatra::Base
    register Sinatra::CrossOrigin

    configure do
        enable :cross_origin

        set :allow_origin, "*" # allows any origin(domain) to send fetch requests to your API
        set :allow_methods, [:get, :post, :patch, :delete, :options] # allows these HTTP verbs
        set :allow_credentials, true
        set :max_age, 1728000
        set :expose_headers, ['Content-Type']
    end

    options '*' do
        response.headers["Allow"] = "HEAD,GET,POST,DELETE,OPTIONS"
        response.headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept"
        200
    end

end
Enter fullscreen mode Exit fullscreen mode

configure do

What is this?

Method: Sinatra::Base.configure

Set configuration options for Sinatra and/or the app. Allows scoping of settings for certain environments.

enable :cross_origin

Configuring Settings

The enable and disable methods are sugar for setting a list of settings to true or false, respectively. The following two code examples below are equivalent:

enable :sessions, :logging
disable :dump_errors, :some_custom_option
Enter fullscreen mode Exit fullscreen mode

Using set:

set :sessions, **true**
set :logging, **true**
set :dump_errors, **false**
set :some_custom_option, **false**
Enter fullscreen mode Exit fullscreen mode

Sinatra's set method

the set method takes a setting name and value and creates an attribute on the application.

Configuring Settings

If you look at the source code for sinatra-cross_origin, you'll see this:

def self.registered(app)

      app.helpers CrossOrigin::Helpers

      app.set :cross_origin, false
      app.set :allow_origin, :any
      app.set :allow_methods, [:post, :get, :options]
      app.set :allow_credentials, true
      app.set :allow_headers, ["*", "Content-Type", "Accept", "AUTHORIZATION", "Cache-Control"]
      app.set :max_age, 1728000
      app.set :expose_headers, ['Cache-Control', 'Content-Language', 'Content-Type', 'Expires', 'Last-Modified', 'Pragma']

      app.before do
        cross_origin if settings.cross_origin
      end

end
Enter fullscreen mode Exit fullscreen mode

Setting enable :cross_origin sets the boolean value to true. As you can see, our code in the application mirrors what you see in the self.registered method. Here's the code again for reference in our configure method:

configure do
    enable :cross_origin # turns app.set :cross_origin, false to be app.set :cross_origin, true

    set :allow_origin, "*" # allows your frontend to send fetch requests
    set :allow_methods, [:get, :post, :patch, :delete, :options] # allows these HTTP verbs
    set :allow_credentials, true
    set :max_age, 1728000
    set :expose_headers, ['Content-Type']
end
Enter fullscreen mode Exit fullscreen mode

On the github page for sinatra-cross_origin, they give a warning:

Responding to OPTIONS
Many browsers send an OPTIONS request to a server before performing a CORS request (this is part of the specification for CORS ). These sorts of requests are called preflight requests. Without a valid response to an OPTIONS request, a browser may refuse to make a CORS request (and complain that the CORS request violates the same-origin policy).
Currently, this gem does not properly respond to OPTIONS requests. See this issue. You may have to add code like this in order to make your app properly respond to OPTIONS requests:

options "*" do
  response.headers["Allow"] = "HEAD,GET,PUT,POST,DELETE,OPTIONS"

  response.headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept"

  200
end
Enter fullscreen mode Exit fullscreen mode

rack-contrib

gem 'rack-contrib'
Enter fullscreen mode Exit fullscreen mode

rack/rack-contrib

This Gem will allow you to parse JSON. You have to include what this gem allows you to use in your rakefile. IN your rakefile you need four things:

use Rack::MethodOverride
use Rack::JSONBodyParser
use UsersController
run ApplicationController
Enter fullscreen mode Exit fullscreen mode

Rack::JSONBodyParser - without this, your application breaks and it won't log in users.

UsersController

ApplicationController

fast_jsonapi

gem 'fast_jsonapi'
Enter fullscreen mode Exit fullscreen mode

This Gem will serialize our json so that we don't have to write it out by hand. You will have to create your serializer model by hand though.

More on JSON serialization:

What is deserialize and serialize in JSON?

Netflix/fast_jsonapi

You don't have to use this gem. You can serialize your json by hand, but this process was just easier and simpler for me to use in this example project.

In rails you could use commands to generate a serializer for a model, but in Sinatra you will have to write it by hand. Its just a few steps you'll need to take:

  1. Create a serializers directory in the app directory
  2. Create a user.rb file
  3. Create a class inside of the user.rb file called UserSerializer
  4. Write this code inside of the class: include FastJsonapi::ObjectSerializer
  5. Now lets write the attributes for the class:
class UserSerializer
    include FastJsonapi::ObjectSerializer
    attributes :name, :username, :created_at, :updated_at
end
Enter fullscreen mode Exit fullscreen mode

What does this do? JSON is JavaScript Object Notation. It is a JavaScript Object, except the whole object is converted into a string. This is what your get route will render on localhost:9393/users

When you render json, you have to choose what attributes you will be rendering on localhost:9393/users.

Without using the fast_jsonapi gem, things can get messy in your json renderings.

sinatra-contrib

gem 'sinatra-contrib', require: false
Enter fullscreen mode Exit fullscreen mode

Sinatra::Contrib

sinatra/sinatra

Sinatra::JSON

Why do you need the required false? Because of this issue:

undefined method desc for Sinatra::Application:Class

OR just require sinatra/base at the top of ApplicationController.

Now let's make a fetch request to your API. Run shotgun in your terminal, open developer tools in your browser console, and use this code:

 fetch('http://localhost:9393/users')
.then(resp => resp.json())
.then(json => console.log(json))
Enter fullscreen mode Exit fullscreen mode

You should see an output like this:

{data: Array(5)}
data: Array(5)
0: {id: "1", type: "user", attributes: {}}
1: {id: "2", type: "user", attributes: {}}
2: {id: "3", type: "user", attributes: {}}
3: {id: "4", type: "user", attributes: {}}
4: {id: "5", type: "user", attributes: {}}
length: 5
__proto__: Array(0)
__proto__: Object
Enter fullscreen mode Exit fullscreen mode

Top comments (0)