Monday, 7 March 2016

How to make REST API in Rails | Part 1

 How to make REST API in Rails

Making a simple API in rails

This is the first part of the tutorial series on "How to make ReST API in Rails". 
There are certain prerequisite which I assume that you had already installed. 
Here is the screencast for this tutorial.
 
 
prerequisites:
Ruby
Rails
Postgresql 

Create the app

$ rails new railsapi -d postgresql --skip-bundle
 
This will create the bare-bones scaffold of the rails application "railsapi" specifying
postgresql to use as database and do not install the gems while making the app.
 
In the Gemfile add gem to serialize the resources we are going to expose on the api

$ gem 'active_model_serializers' 

Also comment the 'jbuilder' and 'turbolinks' gem in the Gemfile, as we are not going
to deal with the frontend part. We will only create the json response which doesn't need these gems. 
Now go inside railsapi directory and install the gems
 
$ cd railsapi
$ bundle install

Making the API

To make api, we will isolate the api controllers under a separate namespace "api" by making a directory 
"api" in "app/controller" directory.
 
$ mkdir app/controllers/api
 
Now we will add this namespace in our routes.rb file
 
Rails.application.routes.draw do 
  namespace :api do

  end 
end 
 
Since we want our responses to be in json, So will set the default format of this api
to json. Change the above code.

Rails.application.routes.draw do 
  namespace :api, defaults:{ format: :json }do

  end 
end 
 
For now, our endpoint url should look like
http://localhost:3000/api/ 

Version the API

We should version our API from the beginning, so that it gives a better structure to our
rails app and in future if we need to add new feature our old code may keep on serving
the older feature while the new version will start serving new feature.

In order to set the version for the api, we first need to add another directory under the api directory
 
$ mkdir app/controllers/api/v1

Since we have another directory in api, we need to tell our routes.rb file to know
about this change. Let's modify routes.rb file

Rails.application.routes.draw do
  # API routes path
  namespace :api, defaults: { format: :json } do
    namespace :v1 do

    end
  end  
end 

So we have added another namespace 'v1' inside api.
For now, our endpoint url should look like
http://localhost:3000/api/v1/

Adding a Model

Now let's add the User model using devise. To use devise add the Gem devise in Gemfile
 
gem "devise"
Now run bundle install to install devise gem. After doing bundle install we need to run the devise install generator  
 
$ rails g devise:install 
Let's generate the user model through the devise generator
 
$ rails g devise User
After doing these, let's now create database and run the migrations 
 
$ rake db:create
$ rake db :migrat

After installing devise the routes.rb should looks like this


Rails.application.routes.draw do
devise_for :users# API routes path
  namespace :api, defaults: { format: :json } do
    namespace :v1 do
      #devise_for :users
      resources :users, :only => [:show, :create]
    end
  end  
end

Building first endpoint

Now we have the User model ready, we will create the first endpoint with the show action
which will return the user record in json format.
First generate users controller 
 
$ rails generate controller users

If the users_controller.rb file is not generated in api/v1/ directory than move the file
inside app/controllers/api/v1/
 
The users_controller.rb file should look like
 
class Api::V1::UsersController < ApplicationController
  respond_to :json

  def show
    respond_with User.find(params[:id])
  end
end  

Here The 'A' of api and 'V' of v1 should be in caps. We have defined the response
format as json. The show action finds the user with the id passed as params and returns
the json representation of it.
 
For now, our endpoint url should look like
http://localhost:3000/api/v1/users/1
where 1 is the id
 
This will give an error now if you run the code as there is no record in the database.
So, Let's create one from rails console
 
$ rails console

> User.create({email: "example@railsapi.com",password: "12345678",password_confirmation: "12345678"}) 
This will create a record with id 1. 
After this you can run rails server and check if our endpoint is working or not

$ rails s

In the new command line tab hit the endpoint to get the data

$ curl localhost:3000/api/v1/users/1
 
Output should be 
 
{"id":1,"email":"example@railsapi.com","created_at":"2016-03-04T23:10:16.169Z","updated_at":"2016-03-04T23:10:16.169Z"}
If you are having problems with the response and have checked everything is well, then you
might need to visit the application_controller.rb file and update it as follows.
 
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session
end
 
we should be using null_session to prevent CSFR attacks , so we should use it in order for POST or PUT requests to work 
 
By now you have learned how to create api with an endpoint. In the next post will learn
how to add more actions like create, update and delete/destroy to our api.
 
Next Chapter 

Further you can dockerize this Rails app - Follow this post to dockerize this API

27 comments:

  1. it is not working for me.. route no match error

    ReplyDelete
    Replies
    1. Hey Khalidh, Can you please exactly specify the error. I doubt you must be have not specified the namespace in routes.rb file. If you have doubt in any steps than you can also watch the video tutorial available on youtube. https://www.youtube.com/watch?v=B-8dJLSB3hU

      Delete
    2. This comment has been removed by the author.

      Delete
  2. Rails.application.routes.draw do
    devise_for :users
    namespace :api, defaults: { format: :json } do
    namespace :v1 do
    resources :users, only: [:show, :update]
    end
    end
    end

    ReplyDelete
  3. I guess we need to add after v1 namespace resources :users, only: [:show, :update]

    ReplyDelete
    Replies
    1. Yes Srinivas, Thanks for pointing it out. :)

      Delete
  4. I am getting LoadError in rails 5.

    fabritronix@fabritronix:~/test/railsapi$ rails s
    => Booting Puma
    => Rails 5.0.0.1 application starting in development on http://localhost:3000
    => Run `rails server -h` for more startup options
    Puma starting in single mode...
    * Version 3.6.0 (ruby 2.3.0-p0), codename: Sleepy Sunday Serenity
    * Min threads: 5, max threads: 5
    * Environment: development
    * Listening on tcp://localhost:3000
    Use Ctrl-C to stop
    Started GET "/api/v1/users/1" for 127.0.0.1 at 2016-09-20 19:08:59 +0530
    ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations"

    LoadError (Unable to autoload constant UsersController, expected /home/fabritronix/test/railsapi/app/controllers/users_controller.rb to define it):

    activesupport (5.0.0.1) lib/active_support/dependencies.rb:512:in `load_missing_constant'
    activesupport (5.0.0.1) lib/active_support/dependencies.rb:203:in `const_missing'
    activesupport (5.0.0.1) lib/active_support/dependencies.rb:543:in `load_missing_

    ReplyDelete
  5. for subsequent requests I am getting output.

    fabritronix@fabritronix:~/test/railsapi$ curl localhost:3000/api/v1/users/1
    {"id":1,"email":"example@railsapi.com","created_at":"2016-09-20T13:00:56.393Z","updated_at":"2016-09-20T13:00:56.393Z"}

    ReplyDelete
    Replies
    1. This is the expected output. What else you are expecting?

      Delete
    2. I did not move the controller to api/v1 directory that is way it was showing Load error.

      Everything works fine.

      Excellent tutorial.

      Keep up the good work.

      Thanks.

      Delete
    3. This comment has been removed by the author.

      Delete
    4. Thank you for the appreciation. "Keep learning and Keep sharing"

      Delete
  6. You need to add this [ "get 'users/:id' => 'users#show' ] in routes.

    the route file looks like this:


    Rails.application.routes.draw do
    devise_for :users
    namespace :api, defaults:{format: :json} do
    namespace :v1 do
    get 'users/:id' => 'users#show'
    end
    end
    end

    ReplyDelete
    Replies
    1. Thanks Jose for pointing it out. Actually the route is added but that piece of code is shown in the second part of this post - https://jee-appy.blogspot.in/2016/03/how-to-make-rest-api-in-rails-part-2.html

      Although am also adding it in this part also.
      Thanks

      Delete
  7. can you look at this problem

    http://stackoverflow.com/questions/41270507/running-curl-on-ruby-on-rails/41271519#41271519

    ReplyDelete
    Replies
    1. I hope your problem is solved by this link http://stackoverflow.com/questions/1195962/submitting-post-data-from-the-controller-in-rails-to-another-website

      Delete
  8. 192-168-1-117:restapi leotyndall$ bundle install
    Could not locate Gemfile
    192-168-1-117:restapi leotyndall$

    This is the error message

    ReplyDelete
    Replies
    1. you need to run bundle install inside your application directory. Make sure you are in the root directory of your project.

      Delete
  9. An error occurred while installing pg (1.0.0), and Bundler cannot continue. any ideas

    ReplyDelete
    Replies
    1. Can you please share more error details. which OS you are using?

      Delete
  10. I am using Mac latest high sierra v10.13.4

    extconf failed, exit code 1

    Gem files will remain installed in /Users/leotyndall/.rbenv/versions/2.4.2/lib/ruby/gems/2.4.0/gems/pg-1.0.0 for inspection.
    Results logged to
    /Users/leotyndall/.rbenv/versions/2.4.2/lib/ruby/gems/2.4.0/extensions/x86_64-darwin-17/2.4.0-static/pg-1.0.0/gem_make.out

    An error occurred while installing pg (1.0.0), and Bundler cannot continue.
    Make sure that `gem install pg -v '1.0.0'` succeeds before bundling.


    ReplyDelete
  11. here is the mkmf.log

    find_executable: checking for pg_config... -------------------- no

    --------------------

    find_header: checking for libpq-fe.h... -------------------- no

    "clang -o conftest -I/Users/leotyndall/.rbenv/versions/2.4.2/include/ruby-2.4.0/x86_64-darwin17 -I/Users/leotyndall/.rbenv/versions/2.4.2/include/ruby-2.4.0/ruby/backward -I/Users/leotyndall/.rbenv/versions/2.4.2/include/ruby-2.4.0 -I. -I/Users/leotyndall/.rbenv/versions/2.4.2/include -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT -O3 -Wno-error=shorten-64-to-32 -pipe conftest.c -L. -L/Users/leotyndall/.rbenv/versions/2.4.2/lib -L. -L/Users/leotyndall/.rbenv/versions/2.4.2/lib -fstack-protector -L/usr/local/lib -lruby-static -framework CoreFoundation -lpthread -ldl -lobjc "
    checked program was:
    /* begin */
    1: #include "ruby.h"
    2:
    3: int main(int argc, char **argv)
    4: {
    5: return 0;
    6: }
    /* end */

    "clang -E -I/Users/leotyndall/.rbenv/versions/2.4.2/include/ruby-2.4.0/x86_64-darwin17 -I/Users/leotyndall/.rbenv/versions/2.4.2/include/ruby-2.4.0/ruby/backward -I/Users/leotyndall/.rbenv/versions/2.4.2/include/ruby-2.4.0 -I. -I/Users/leotyndall/.rbenv/versions/2.4.2/include -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT -O3 -Wno-error=shorten-64-to-32 -pipe conftest.c -o conftest.i"
    conftest.c:3:10: fatal error: 'libpq-fe.h' file not found
    #include
    ^~~~~~~~~~~~
    1 error generated.
    checked program was:
    /* begin */
    1: #include "ruby.h"
    2:
    3: #include
    /* end */

    --------------------

    ReplyDelete
  12. using ruby 2.4.2 and rails 5.2.0

    ReplyDelete

 

Copyright @ 2013 Appychip.

Designed by Appychip & YouTube Channel