--- title: API Development tags: API, Ruby, Ruby on Rails description: Step by step API development with Ruby on Rails. --- [![hackmd-github-sync-badge](https://hackmd.io/MLgY349iTpWj8x3IUvytfQ/badge)](https://hackmd.io/MLgY349iTpWj8x3IUvytfQ) <style> code.blue { color: #337AB7 !important; } code.orange { color: #F7A004 !important; } code.green { color: #008000 !important; } code.red { color: #FF0000 !important; } </style> # To the :moon: ! <!-- Put the link to this slide here so people can follow --> slide: https://hackmd.io/@hanmaslah/H1VvcpP7O --- We have a collaborative session Please prepare laptop to join! --- ## Who am I? - Senior Ruby on Rails developer :female-astronaut: - VSCode :heart: - I use tabs. :cat: - Ruby is my love :sunflower: --- ## Developers :heart: GitHub. If you haven't done so, follow the tutorial about WLS and GIT and Rails Installation --- Remind me to put a video here <!-- {%youtube E8Nj7RwXf0s %} --> --- # Table of Contents - Create a rails app, db=pg, api-only - Create User model - username and password - Create conversation model - owner - Create messages model - conversation, author, body --- Create the API ```shell= rails new forum --api --skip-test-unit --database=postgresql ``` --- CD into the Rails folder ```shell= cd forum ``` --- Create the database ```shell= rails db:create ``` --- Always GIT everything. If GIT is not initiated by default, git init and link with remote repository ```shell= git init git remote add origin git@github.com:hmasila/forum.git ``` --- Commit the generated files ```shell= git add . git commit -m 'initial commit' git push origin master ``` --- <code class="blue">Do a happy dance and get back to work</code> ![Ruby dance](https://media1.tenor.com/images/6445ee2274a782a7c528303e9bd823d7/tenor.gif?itemid=9878005) --- # Models --- Rails uses [Active Record](https://guides.rubyonrails.org/active_record_basics.html) as the ORM Framework Model is the layer of the system responsible for representing business data and logic. --- We have 3 models. The user, conversation and messages <code class="orange"> - A conversation belongs to the owner - A conversation has many users through messages - A conversation has many messages - A user has many authored_conversations as author - A user has many messages as author - A user has many conversations through messages - A message belongs to a conversation - A message belongs to a user as contributor </code> --- ## User --- Generate the user model ```cmd= rails generate scaffold User username:string password_digest:string avatar_url:string ``` --- This will generate a bunch of files. In a rails app, scaffold follows the MVC architecture but since we are creating an API, we only have M and C So the files created will be the model, controller, database migration and the routes file is updated to include the user's routes. --- The migration file looks like this ```ruby class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :username, null: false, index: { unique: true } t.string :password_digest, null: false t.string :avatar_url t.timestamps end end end ``` --- Run the migration. This will update the schema with the new data. ```shell= rails db:migrate ``` --- The password_digest attribute depends on bcrypt Add the following line to your Gemfile ```ruby= gem 'bcrypt', '~> 3.1.7' ``` --- Run bundler ```shell= bundle install ``` --- ### Validations --- Add these lines to your User model ```ruby has_secure_password validates :username, presence: true, uniqueness: { case_sensitive: false } ``` --- [Secure Password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html) creates the following validations: - Password must be present on creation - Password length should be less than or equal to 72 bytes - Confirmation of password (using a password_confirmation attribute) The username validation makes sure we only allow unique usernames to be created. It also makes sure the uniqueness is case insensitive. If we already have <code class="orange">JohnDoe</code> then <code class="orange">johndoe</code> is not acceptable --- <code class="green">app/models/user.rb</code> ```ruby class User < ApplicationRecord has_many :messages, inverse_of: :author, foreign_key: :author_id has_many :authored_conversations, inverse_of: :author, class_name: 'Conversation' has_many :conversations, through: :messages has_secure_password validates :username, presence: true, uniqueness: { case_sensitive: false } end ``` --- At this point you can commit the new changes. --- ## Conversation --- Generate the conversation model ```shell= rails g scaffold Conversation user:references ``` --- The migration file looks like this ```ruby class CreateConversations < ActiveRecord::Migration[6.0] def change create_table :conversations do |t| t.references :user, null: false, foreign_key: true t.timestamps end end end ``` --- <code class="green">app/models/conversation.rb</code> ```ruby class Conversation < ApplicationRecord has_many :messages has_many :users, through: :messages, source: :author belongs_to :author, class_name: 'User', inverse_of: :authored_conversations, foreign_key: :user_id end ``` --- Run the migrations and commit the changes --- ## Message --- Generate the message model ```shell= rails g scaffold Message conversation:references user_id:integer body:text ``` --- The migration file looks like this ```ruby class CreateMessages < ActiveRecord::Migration[6.0] def change create_table :messages do |t| t.references :conversation, null: false, foreign_key: true t.integer :contributor_id t.text :body t.timestamps end end end ``` --- <code class="green">app/models/message.rb</code> ```ruby class Message < ApplicationRecord belongs_to :conversation belongs_to :author, class_name: 'User', inverse_of: :messages end ``` --- Run the migrations and commit the changes --- # Console --- To test out the relationships, use rails console ```shell= rails console ``` --- ## Create Users --- ```ruby= User.create!(username: 'skywalker', password: 'passworD314', password_confirmation: 'passworD314') User.create!(username: 'hansolo', password: 'passworD314', password_confirmation: 'passworD314') User.create!(username: 'rookie', password: 'passworD314', password_confirmation: 'passworD314') ``` --- Try creating a user that doesn't meet the validations. Examples ```ruby= # username already taken User.create!(username: 'rookie', password: 'passworD314', password_confirmation: 'passworD314') # no username User.create!(password: 'passworD314', password_confirmation: 'passworD314') # no password User.create!(username: 'noPassword') # wrong confirmation User.create!(username: 'skywalker', password: 'passworD314', password_confirmation: 'wrongPassword') ``` --- ## Create Conversation --- ```ruby= # using user_id Conversation.create(user_id: 1) # using user user = User.find(1) user.authored_conversations.create! ``` --- ## Create Messages --- ```ruby= Message.create!(conversation_id: 1, author_id: 1, body: "I'm Luke Skywalker, I'm Here To Rescue You.") # using conversation conversation = Conversation.find(1) conversation.messages.create(author_id: 2, body: "Hokey religions and ancient weapons are no match for a good blaster at your side, kid.") # using user user = User.find_by(username: 'skywalker') user.messages.create(conversation_id: 1, body: "Strike Me Down In Anger And I'll Always Be With You.") ``` --- ## Reading from Database --- Here is some of the data we can retrieve ```ruby= user = User.find_by(username: 'skywalker') conversation = Conversation.find(1) message = conversation.messages.last user.authored_conversations user.conversations user.messages conversation.messages conversation.users conversation.author message.author message.conversation ``` --- # Server --- To start the server, run ```shell= rails server ``` By now, you should be able to test out a few endpoints. --- We will be using [Postman](https://www.postman.com/) for API testing, so make sure you have the app or the agent installed. Open postman and let us try to get users. Enter [http://localhost:3000/users](http://localhost:3000/users) in the url field Pick `GET` from the verb drop down. Click `Send` and you should get a list of all the users. --- ![](https://i.imgur.com/EJwqzfI.png) --- <code class="orange">You have successfully made an API!! 🥳 </code> <code class="blue">Take a break. Stretch. Drink some water.</code> ![Ruby water break](https://media0.giphy.com/media/Bqn8Z7xdPCFy0/giphy.gif?cid=ecf05e472ewz8giaemn6n6rwtajunolak01pziyvqewdnso9&rid=giphy.gif&ct=g) --- # Authentication --- Token-based authentication is stateless authentication commonly implemented using JSON Web Authentication(JWT). It does not store anything on the server but creates a unique encoded token that gets checked every time a request is made. Unlike session-based authentication, a token approach would not associate a user with login information but with a unique token that is used to carry client-host transactions. --- Add this to the Gemfile ```ruby= gem 'jwt', '~> 2.2.3' ``` --- Bundle it ```shell= bundle install ``` --- Create a singleton class for the jwt_token_authentication <code class="green">lib/json_web_token.rb</code> ```ruby #lib/json_web_token.rb class JsonWebToken class << self def encode(payload, exp = 24.hours.from_now) payload[:exp] = exp.to_i JWT.encode(payload, Rails.application.secrets.secret_key_base) end def decode(token) body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0] HashWithIndifferentAccess.new body rescue nil end end end ``` --- We will be using the user ID, the expiration time (1 day), and the unique base key of your Rails application to create a unique token. We will use this method for authenticating the user and generating a token for him/her using encode. --- To decode the token, we will use the application's secret key. We will use this method to check if the user's token appended in each request is correct. --- Add the jwt singleton class to the application to be loaded <code class="green">config/application.rb</code> ```ruby config.autoload_paths << Rails.root.join("lib") ``` --- We will be using `simple_command` gem which facilitates the connection between the controller and the model. Add this to the Gemfile and bundle install ```ruby= gem 'simple_command', '~> 0.1.0' ``` Create a folder inside `app` and name it <code class="orange">commands</code> --- ## Authenticate user This class will be called when the user is loging in. It's aim is to make sure the correct credentials are provided and return a token that will be used for other requests. <code class="green">app/commands/authenticate_user.rb</code> ```ruby # app/commands/authenticate_user.rb class AuthenticateUser prepend SimpleCommand def initialize(username, password) @username = username @password = password end def call JsonWebToken.encode(user_id: user.id) if user end private attr_accessor :username, :password def user user = User.find_by(username: username) return user if user && user.authenticate(password) errors.add :user_authentication, 'invalid credentials' nil end end ``` Prepend SimpleCommand in the simple_command class so that you can be able to use them in the controller classes. --- ## Authorization This class will be called when a request is made. It checks if a token has been provided in the header and decodes it to make sure it is valid. ```ruby # app/commands/authorize_request.rb class AuthorizeRequest prepend SimpleCommand def initialize(headers = {}) @headers = headers end def call user end private attr_reader :headers def user @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token @user || errors.add(:token, 'Invalid token') && nil end def decoded_auth_token @decoded_auth_token ||= JsonWebToken.decode(http_auth_header) end def http_auth_header if headers['Authorization'].present? return headers['Authorization'].split(' ').last else errors.add(:token, 'Missing token') end nil end end ``` > [https://www.pluralsight.com/guides/token-based-authentication-with-ruby-on-rails-5-api] --- Http requests have fields known as headers. Headers can contain a wide variety of information about the request that can be helpful for the server interpreting the request. Tokens are usually attached to the 'Authorization' header. The command for authorization has to take the headers of the request and decode the token using the decode method in the JsonWebToken singleton. --- ## Login This will be our first API endpoint. Users will use it to get a token that they can pass in the headers. ```ruby # app/controllers/authentication_controller.rb class AuthenticationController < ApplicationController skip_before_action :authenticate_request def authenticate command = AuthenticateUser.call(params[:username], params[:password]) if command.success? render json: { auth_token: command.result } else render json: { error: command.errors }, status: :unauthorized end end end ``` --- ## Authorize Requests To put the token to use, we will use a `current_user` method that will be the logged in user. In order to have current_user available to all controllers, it has to be declared in the ***ApplicationController*** --- ```ruby #app/controllers/application_controller.rb class ApplicationController < ActionController::API before_action :authenticate_request attr_reader :current_user private def authenticate_request @current_user = AuthorizeRequest.call(request.headers).result render json: { error: 'Not Authorized' }, status: 401 unless @current_user end end ``` --- By using `before_action`, the server passes the request headers (using the built-in object property request.headers) to <code class="green">AuthorizeRequest</code> every time the user makes a request. Calling result on <code class="green">AuthorizeRequest.call(request.headers)</code> is coming from SimpleCommand module where it is defined as attr_reader :result. The request results are returned to the <code class="green">@current_user</code>, thus becoming available to all controllers inheriting from ApplicationController. --- ## Testing Open Postman and send a request to retrieve users like we did before. You should receive `unauthorized` error --- ![](https://i.imgur.com/Yvwexnn.png) --- <code class="red">That's a lot to take in. Commit the changes and take a break.</code> ![Ruby water break](https://media2.giphy.com/media/3o6Ztd0zUjRtdKlk1q/giphy.gif?cid=ecf05e47h8fv2mwq8yzg8d7290uufc2xz6x5yb1rataqvair&rid=giphy.gif&ct=g) --- # Routes The pattern for routes is usually the - `verb` - they specify an action to be performed on a specific resource or a collection of resources. - `path` - for example 'authenticate' --- - `to` - this maps the request to a method. The value is divided into two. The first part is the name of the controller and the second part is the method, Therefore <code class="green">'home#test'</code> will go to home_controller and check for test method To refer to a route, we use the path name. For example: <code class="green">post 'home/test', to: home#test'</code> will be <code class="orange">home_test_path</code> --- - `as` - Gives an alias to the path When using `as`, you can define how you want to reference your path. For example: <code class="green">post 'branches/test', to: home#test', as: 'test'</code> will be <code class="orange">test_path</code> instead of <code class="orange">home_test_path</code> --- - `resources` - This is used when you want to create routes for a resource. By default, Rails gives us access to the `CRUD` routes. You can decide the routes to keep by using keywords like: - `only` - this will only create routes for the defined methods. For example: <code class="green">resources 'users', only: 'index'</code> - this will only create a route for the index method in the user's controller. - `except` - this will create the CRUD endpoints except the ones you have specified. In API mode, Rails gives us <code class="orange">index, create, show, update, delete</code>. In a Rails app, you have two more methods which are <code class="green">edit, new </code> --- ## Verbs --- ### post - create a new resource. Modifies the database --- ### get - retrieves resource representation/information and does not modify it in any way. --- ### put - update existing resource. POST requests are made on resource collections, whereas PUT requests are made on a single resource. --- ### patch - make partial update on a resource. For example updating the password of a user without affecting the other attributes. --- ### delete - delete resources (identified by the Request-URI. Usually the id of the resource to be deleted) [Read more](https://restfulapi.net/http-methods/) --- Let us add the authentication route ```ruby Rails.application.routes.draw do post 'authenticate', to: 'authentication#authenticate' resources :messages resources :conversations resources :users end ``` --- ## Testing Authentication Open Postman Enter [http://localhost:3000/authenticate](http://localhost:3000/authenticate) as the path Choose `POST` from the verb drop down Click on body and pick raw as the body format. Then on the drop down on your right, choose `JSON` Paste this ```json= { "username": "skywalker", "password": "princess123" } ``` --- ![](https://i.imgur.com/2pEDYVc.png) --- Let us try with the correct password Repeat the process above but replace the password with `passworD314` or the correct password for the user you're trying to authenticate. This should return the auth token as shown below. --- ![](https://i.imgur.com/zcmkNjO.png) --- ## Authorizing Requests To get rid of the `unauthorized` error we got before the break, we need to insert the token in the headers. In your Postman, open the tab written headers. Enter `Authorization` as the key and the auth_token result from authentication above as the value. Note: Whenever you get unauthorized or expired token error, authenticate again and replace the authentication token with the new one. --- ![](https://i.imgur.com/aa6QUBq.png) --- ## Response Code Visit [this link](https://dev.to/khaosdoctor/the-complete-guide-to-status-codes-for-meaningful-rest-apis-1-5c5) to learn about the HTTP status codes. --- To get a list of all the routes in your application, run ```shell= rails routes ``` For more details on routes, visit the [Rails documentation](https://guides.rubyonrails.org/routing.html) --- It's time for another break! Shake! Shake! Shake! ![](https://media3.giphy.com/media/mFeEjqUVMfjpilVGS3/giphy.gif?cid=ecf05e477wx3t0b4wnql8gx7fs5wxyy40om9foa7w8rhejmb&rid=giphy.gif&ct=g) --- # Controllers --- Rails uses [Action Controller](https://guides.rubyonrails.org/action_controller_overview.html). The controller will receive the request, fetch or save data from a model, and return an appropriate response to the user. A controller can thus be thought of as a middleman between models and user. It makes the model data available to the user, and it saves or updates user data to the model. --- ## CRUD APIs perform the following operations - **C => Create** - **R => Read** - **U => Update** - **D => Delete** --- In Rails logic, we have 5 main controller methods <code class="green">create</code> - This method creates a new record for the resource - Verb: POST --- <code class="blue">index</code> - This method returns resource records - Verb: GET ---- <code class="blue">show</code> - This method returns an instance of a resource. - Verb: GET --- <code class="orange">update</code> - This method performs an update on a record of the resource - Verb: PUT/PATCH --- <code class="red">delete</code> - This method deletes a record from the database - Verb: DELETE --- <code class="blue">new</code> - In a full-stack rails application, this method will render a template for collecting a new record for the resource. It is not applicable in an API - Verb: GET --- <code class="blue">edit</code> - In a full-stack rails application, this method will render a template for editing a record for the resource. It is not applicable in an API - Verb: GET --- For our API, let's move our controller to api/v1 which will be our namespace. We should also update the routes <code class="green">config/routes.rb</code> ```ruby Rails.application.routes.draw do post 'authenticate', to: 'authentication#authenticate' namespace :api, defaults: { format: :json } do namespace :v1 do resources :messages resources :conversations resources :users end end end ``` Remember to add the namespace to your controllers as well. --- ## Users controller --- We won't be returning all the users therefore we can remove the `index` method ```ruby # app/controllers/api/v1/users_controller.rb class Api::V1::UsersController < ApplicationController skip_before_action :authenticate_request, only: [:create] before_action :set_user, only: [:show, :update, :destroy] # GET /users/1 def show render json: @user end # POST /users def create @user = User.new(user_params) if @user.save render json: @user, status: :created else render json: @user.errors, status: :unprocessable_entity end end # PATCH/PUT /users/1 def update if @user.update(user_params) render json: @user else render json: @user.errors, status: :unprocessable_entity end end # DELETE /users/1 def destroy @user.destroy end private # Use callbacks to share common setup or constraints between actions. def set_user @user = User.find(params[:id]) end # Only allow a trusted parameter "white list" through. def user_params params.require(:user).permit(:username, :password, :password_confirmation, :avatar_url) end end ``` --- We also need to update the routes to reflect this change. <code class="green">config/routes.rb</code> ```ruby Rails.application.routes.draw do post 'authenticate', to: 'authentication#authenticate' namespace :api, defaults: { format: :json } do namespace :v1 do resources :messages resources :conversations resources :users, only: [:create, :show, :update, :delete] end end end ``` --- ## Testing Route changes To test the changes of anything outside the App folder, you will have to restart the server. Stop the server by pressing `ctrl + c` And start the server again by typing `rails server` --- Open Postman Enter [http://localhost:3000/users](http://localhost:3000/users) in the url field Pick `GET` from the verb drop down. Click `Send` and you should get a list of all the users. You will get an error message because this path no longer exists. --- ![](https://i.imgur.com/RvTZbeV.png) --- ## Conversations Controller We only want to display conversations that belong to a user. In the conversations controller, edit the calls to the model to only refer to the current_user's records. For example, instead of <code class="red">Conversation.all</code>, we will have <code class="blue">@current_user.conversations</code> --- Let us separate the conversations that the current user has contributed to and those that they have started. Add a method called <code class="green">authored</code> method that will return the conversations that the current user has started. The index method will return all conversations that the current user has interacted with. Let us also make sure the current user can only delete or update conversations that they have authored. --- ```ruby class Api::V1::ConversationsController < ApplicationController before_action :set_conversation, only: [:show] before_action :set_authored_conversation, only: [:authored_conversation, :update, :destroy] # GET /conversations def index @conversations = @current_user.conversations render json: @conversations end # GET /conversations/authored def authored_conversations @authored_conversations = @current_user.authored_conversations render json: @authored_conversations end # GET /conversations/authored/1 def authored_conversation render json: @authored_conversation end # GET /conversations/1 def show render json: @conversation end # POST /conversations def create @authored_conversation = @current_user.authored_conversations.new if @authored_conversation.save render json: @authored_conversation, status: :created else render json: @authored_conversation.errors, status: :unprocessable_entity end end # PATCH/PUT /conversations/1 def update if @authored_conversation.update(conversation_params) render json: @authored_conversation else render json: @authored_conversation.errors, status: :unprocessable_entity end end # DELETE /conversations/1 def destroy @authored_conversation.destroy end private def set_conversation @conversation = @current_user.conversations.find(params[:id]) end def set_authored_conversation @authored_conversation = @current_user.authored_conversations.find(params[:id]) end end ``` --- Add the new endpoints to <code class="green">routes.rb</code> Restart the server and open Postman Enter [http://localhost:3000/api/v1/conversations](http://localhost:3000/api/conversations) in the url field You will get all the conversations the user has been part of. Remember if you get unauthorized, you have to authenticate the user and update the authorization token --- ![](https://i.imgur.com/KwOmBe2.png) --- <code class="blue">To get the conversations the user has authored</code> ![](https://i.imgur.com/6QXffoB.png) --- <code class="blue">To get a specific conversation that the user has authored</code> ![](https://i.imgur.com/WdsKZ1Z.png) --- <code class="green">To initiate a conversation</code> ![](https://i.imgur.com/YRjK4aT.png) --- # Serializer! :8ball: :1234: To be continued ...