API Creation With Sinatra
Tags: sinatra, ruby, api, cors
Published: 2020-07-20 19:47:09

### 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:

- https://github.com/thebrianemory/corneal - The Corneal Gem isn't up to date, but it is still useful in generating a new Sinatra App from scratch.
- https://thebrianemory.github.io/corneal/

**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:

```ruby
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
```

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

```ruby
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
```

### 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:

```ruby
gem 'activerecord', '~> 6.0', '>= 6.0.3.2', :require => 'active_record'
```

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.

```ruby
rake db:create_migration NAME=create_users
```

Now we have a new empty migration table.

```ruby
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
  end
end
```

Lets add some user information.

```ruby
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
```

`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](https://github.com/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](https://edgeguides.rubyonrails.org/active_record_migrations.html#migration-overview)

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:

```ruby
group :development do
  gem 'pry'
  gem 'tux'
  gem 'sqlite3'
  gem 'faker'
end
```

### 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](http://coding.jandavid.de/2016/02/08/how-to-set-up-sinatra-with-activerecord/)

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

```ruby
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' %>
```

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:

```ruby
5.times do
    User.create(
        name: Faker::Name.name,
        username: Faker::Internet.username,
        password: Faker::Internet.password
    )
end
```

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`:

```ruby
desc "A console"
task :console do
  Pry.start
end
```

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.

```ruby
gem "sinatra-cross_origin"
gem 'rack-contrib'
gem 'fast_jsonapi'
gem 'sinatra-contrib', require: false
```

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

### What do these gems do?

# sinatra-cross_origin

```ruby
gem "sinatra-cross_origin"
```

[britg/sinatra-cross_origin](https://github.com/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:

```ruby
register Sinatra::CrossOrigin
```

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:

```ruby
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
```

## `configure do`

What is this?

[Method: Sinatra::Base.configure](https://www.rubydoc.info/gems/sinatra/Sinatra%2FBase.configure)

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

## `enable :cross_origin`

[Configuring Settings](http://sinatrarb.com/configuration.html)

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:

```ruby
enable :sessions, :logging
disable :dump_errors, :some_custom_option
```

Using `set`:

```ruby
set :sessions, **true**
set :logging, **true**
set :dump_errors, **false**
set :some_custom_option, **false**
```

## Sinatra's `set` method

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

[Configuring Settings](http://sinatrarb.com/configuration.html)

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

```ruby
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
```

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:

```ruby
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
```

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](http://www.w3.org/TR/cors/) ). These sorts of requests are called [preflight requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_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](https://github.com/britg/sinatra-cross_origin/issues/18). You may have to add code like this in order to make your app properly respond to `OPTIONS` requests:

```ruby
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
```

# rack-contrib

```ruby
gem 'rack-contrib'
```

[rack/rack-contrib](https://github.com/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:

```ruby
use Rack::MethodOverride
use Rack::JSONBodyParser
use UsersController
run ApplicationController
```

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

`UsersController`

`ApplicationController`

# fast_jsonapi

```ruby
gem 'fast_jsonapi'
```

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?](https://stackoverflow.com/questions/3316762/what-is-deserialize-and-serialize-in-json#answer-3316779)

[Netflix/fast_jsonapi](https://github.com/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:

```ruby
class UserSerializer
    include FastJsonapi::ObjectSerializer
    attributes :name, :username, :created_at, :updated_at
end
```

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

```ruby
gem 'sinatra-contrib', require: false
```

[Sinatra::Contrib](http://sinatrarb.com/contrib/)

[sinatra/sinatra](https://github.com/sinatra/sinatra/tree/master/sinatra-contrib)

[Sinatra::JSON](http://sinatrarb.com/contrib/json)

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

[undefined method `desc` for Sinatra::Application:Class](https://stackoverflow.com/questions/20638136/undefined-method-desc-for-sinatraapplicationclass#answer-20642616)

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: 
```javascript
 fetch('http://localhost:9393/users')
.then(resp => resp.json())
.then(json => console.log(json))
```

You should see an output like this:
```javascript
{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
```