{"id":15968940,"url":"https://github.com/vasilakisfil/rails5_api_tutorial","last_synced_at":"2025-04-06T03:10:28.697Z","repository":{"id":43139904,"uuid":"64008389","full_name":"vasilakisfil/rails5_api_tutorial","owner":"vasilakisfil","description":"Learn how to build a modern API on Michael Hartl's Rails 5 tutorial","archived":false,"fork":false,"pushed_at":"2019-12-02T20:12:31.000Z","size":591,"stargazers_count":453,"open_issues_count":1,"forks_count":44,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-30T02:07:34.216Z","etag":null,"topics":["api","rails","tutorial"],"latest_commit_sha":null,"homepage":"https://vasilakisfil.github.io/rails5_api_tutorial","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vasilakisfil.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-07-23T09:06:48.000Z","updated_at":"2025-02-06T17:56:27.000Z","dependencies_parsed_at":"2022-08-30T03:01:42.355Z","dependency_job_id":null,"html_url":"https://github.com/vasilakisfil/rails5_api_tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vasilakisfil%2Frails5_api_tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vasilakisfil%2Frails5_api_tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vasilakisfil%2Frails5_api_tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vasilakisfil%2Frails5_api_tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vasilakisfil","download_url":"https://codeload.github.com/vasilakisfil/rails5_api_tutorial/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247427006,"owners_count":20937201,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","rails","tutorial"],"created_at":"2024-10-07T19:04:34.818Z","updated_at":"2025-04-06T03:10:28.673Z","avatar_url":"https://github.com/vasilakisfil.png","language":"Ruby","readme":"# Build an API in your Rails app now! (Rails 5 version)\n\n_Note 1: If you are looking for the regular readme, it's [here](docs/README.md)._\n\n_Note 2: You can contribute to this tutorial by opening an issue or even sending a pull request!_\n\n_Note 3: With the API I built, I went on and created the [same app](https://github.com/vasilakisfil/ember_on_rails5) in Ember._\n\nI will show how you can extend your Rails app and build an API without\nchanging a single line of code from your existing app.\nWe will be using [Michael Hartl's Rails tutorial](https://www.railstutorial.org/)\n(I actually started learning Rails and subsequently Ruby from that tutorial, I really owe a\nbeer to that guy) which is a classic [Rails app](https://bitbucket.org/railstutorial/sample_app_4th_ed)\nand extend it by building an API for the app.\n\n\n## Designing our API\nDesigning an API is not an easy process.\nUsually it's very difficult to know beforehand what the client will need.\nHowever we will make our best to support most clients needs:\n\n  * have a resty approach using the popular JSONAPI spec\n  * use hypermedia for related resources instead of embedding them\n  * have in the same response data that otherwise would require many requests in the client\n\nBy the way, there is a long discussion about what REST means. Is just JSONAPI as REST as Joy Fielding's defined it?\nDefinitely not. However, it's more resty than regular JSON response, plus it has a wide support in terms of libraries.\n\nMoving forward, let's add our first resource, let it be a user. But before adding the controller let's add the routes first:\n\n``` ruby\n  #api\n  namespace :api do\n    namespace :v1 do\n      resources :sessions, only: [:create, :show]\n      resources :users, only: [:index, :create, :show, :update, :destroy] do\n        post :activate, on: :collection\n        resources :followers, only: [:index, :destroy]\n        resources :followings, only: [:index, :destroy] do\n          post :create, on: :member\n        end\n        resource :feed, only: [:show]\n      end\n      resources :microposts, only: [:index, :create, :show, :update, :destroy]\n    end\n  end\n```\n\nAll REST routes for each record, only GET method for collections (Rails muddles up collection REST routes with\nelement REST routes in the same controllers) and a couple custom routes.\n\n\nAs you can see we have many routes. The idea is that the tutorial will mostly touch\nand show you a couple of them and you will manage to understand and see the rest from\nthe code inside the repo. I think extended tutorials are boring :).\nHowever, if you find something weird or you don't understand something you are always welcomed to\nopen an issue and ask :)\n\nLet's create the users API controller and add support for the GET method on a single record:\n\n## Adding our first API resource\n\nThe first thing we need to do is to separate our API from the rest of the app.\nIn order to do that we will create a new Controller under a different namespace.\nGiven that it's good to have versioned API let's go and create our first controller\nunder `app/controllers/api/v1/`\n\n``` ruby\nclass Api::V1::BaseController \u003c ActionController::API\nend\n```\n\nAs you can see we inherit from `ActionController::API` instead of `ActionController::Base`.\nThe former cuts down some features not needed making it a bit faster and less memory hungry :)\n\nNow let's add the `users#show` action:\n\n``` ruby\nclass Api::V1::UsersController \u003c Api::V1::BaseController\n  def show\n    user = User.find(params[:id])\n\n    render jsonapi: user, serializer: Api::V1::UserSerializer\n  end\nend\n```\n\nOne thing that I like building APIs in Rails is that controllers are super clean _by default_.\nWe just request the user from the database and render it in JSON using AMS.\n\nLet's add the user serializer under `app/serializers/api/v1/user_serializer.rb`.\nWe will use [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) for the JSON serialization.\n\n``` ruby\nclass Api::V1::UserSerializer \u003c ActiveModel::Serializer\n  attributes(*User.attribute_names.map(\u0026:to_sym))\n\n  has_many :followers, serializer: Api::V1::UserSerializer\n\n  has_many :followings, key: :followings, serializer: Api::V1::UserSerializer\nend\n```\n\nIf we now request a single user it will also render all followers and followings (users that the user follows).\nUsually we don't want that but instead we probably want AMS to render only the url for the client to fetch the data\nasynchronously. Let's change that and also add a link for Microposts (more info you can find on `active_model_serializers` wiki):\n\n``` ruby\nclass Api::V1::UserSerializer \u003c ActiveModel::Serializer\n  attributes(*User.attribute_names.map(\u0026:to_sym))\n\n  has_many :microposts, serializer: Api::V1::MicropostSerializer do\n    include_data(false)\n    link(:related) {api_v1_microposts_path(user_id: object.id)}\n  end\n\n  has_many :followers, serializer: Api::V1::UserSerializer do\n    include_data(false)\n    link(:related) {api_v1_user_followers_path(user_id: object.id)}\n  end\n\n  has_many :followings, key: :followings, serializer: Api::V1::UserSerializer do\n    include_data(false)\n    link(:related) {api_v1_user_followings_path(user_id: object.id)}\n  end\nend\n```\n\nThere is one more thing that needs to be fixed.\nIf a client asks for a user that does not exist in our database, `find` will raise a `ActiveRecord::RecordNotFound`\nexception and Rails will return a 500 error.\nBut what we actually want here is to return a 404 error.\nWe can catch the exception in the `Api::V1::BaseController` and make Rails return 404.\nJust add in `Api::V1::BaseController`:\n\n``` ruby\n  rescue_from ActiveRecord::RecordNotFound, with: :not_found\n\n  def not_found\n    return api_error(status: 404, errors: 'Not found')\n  end\n```\n\nA \"Not found\" in the body section is enough since the client can figure out the error from the 404 status code.\n\n_Tip: Exceptions in Ruby are quite slow. A faster way is to request the user from the db using find_by and render 404 if find_by returned a nil._\n\n**Important! [yuki24](https://github.com/yuki24) opened [an issue](https://github.com/vasilakisfil/rails5_api_tutorial/issues/15) to clarify that \"rescue_from is possibly one of the worst Rails patterns of all time\". Please take a look in [the issue](https://github.com/vasilakisfil/rails5_api_tutorial/issues/15) for more information until we have something better :)**\n\nIf we now send a request `api/v1/users/1` we get the following json response:\n\n``` http\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"users\",\n    \"attributes\": {\n      \"name\": \"Example User\",\n      \"email\": \"example@railstutorial.org\",\n      \"created-at\": \"2016-11-05T10:15:26Z\",\n      \"updated-at\": \"2016-11-19T21:30:10Z\",\n      \"password-digest\": \"$2a$10$or7HFYm/H07/uE79wDae3uXMmHOX3BvRKdgedPJ1SPceiMA40V25O\",\n      \"remember-digest\": null,\n      \"admin\": true,\n      \"activation-digest\": \"$2a$10$X5IeDtGZPuZQEVQ.ZiUP4eUzfw9M9Pag/nR.0ONiXwAAp3w98iAuC\",\n      \"activated\": true,\n      \"activated-at\": \"2016-11-05T10:15:26.300Z\",\n      \"reset-digest\": null,\n      \"reset-sent-at\": null,\n    },\n    \"relationships\": {\n      \"microposts\": {\n        \"links\": {\n          \"related\": \"/api/v1/microposts?user_id=1\"\n        }\n      },\n      \"followers\": {\n        \"links\": {\n          \"related\": \"/api/v1/users/1/followers\"\n        }\n      },\n      \"followings\": {\n        \"links\": {\n          \"related\": \"/api/v1/users/1/followings\"\n        }\n      }\n    }\n  }\n}\n```\n\nOf course we need to add Authentication and Authorization on our API but we will take\na look on that later :)\n\n## Adding the index method\nNow let's add a method to retrieve all users. Rails names that method index, in terms of REST it's a GET method that acts on the `users` collection.\n\n``` ruby\nclass Api::V1::UsersController \u003c Api::V1::BaseController\n  def index\n    users = User.all\n\n    render jsonapi: users, each_serializer: Api::V1::UserSerializer,\n  end\nend\n```\n\nPretty easy right?\n\n\n## Adding Authentication\nFor authentication, the Rails app by Michael uses a custom implementation.\nThat shouldn't be a problem because we build an API and we need to re-implement the authentication endpoint anyway.\nIn APIs we don't use cookies and we don't have sessions.\nInstead, when a user wants to sign in she sends an HTTP POST request with her username and password to our API (in our\ncase it's the `sessions` endpoint) which sends back a token.\nThis token is user's proof of who she is.\nIn each API request, rails finds the user based on the token sent.\nIf no user found with the received token, or no token is sent, the API should return a 401 (Unauthorized) error.\n\nLet's add the token to the user.\n\nFirst we add a callback that adds a token to every new user is created.\n\n``` ruby\n  before_validation :ensure_token\n\n  def ensure_token\n    self.token = generate_hex(:token) unless token.present?\n  end\n\n  def generate_hex(column)\n    loop do\n      hex = SecureRandom.hex\n      break hex unless self.class.where(column =\u003e hex).any?\n    end\n  end\n```\n\nand exactly after that we create the migration:\n\n```ruby\nclass AddTokenToUsers \u003c ActiveRecord::Migration[5.0]\n  def up\n    add_column :users, :token, :string\n\n    User.find_each{|user| user.save!}\n\n    change_column_null :users, :token, false\n  end\n\n  def down\n    remove_column :users, :token, :string\n  end\nend\n```\n\nand run `bundle exec rails db:migrate`. Now every user, new and old, has a\nvalid unique non-null token.\n\nThen let's add the `sessions` endpoint:\n\n```ruby\nclass Api::V1::SessionsController \u003c Api::V1::BaseController\n  def create\n    if @user\n      render(\n        jsonapi: @user,\n        serializer: Api::V1::SessionSerializer,\n        status: 201,\n        include: [:user],\n        scope: @user\n      )\n    else\n      return api_error(status: 401, errors: 'Wrong password or username')\n    end\n  end\n\n  private\n    def create_params\n      normalized_params.permit(:email, :password)\n    end\n\n    def load_resource\n      @user = User.find_by(\n        email: create_params[:email]\n      )\u0026.authenticate(create_params[:password])\n    end\n\n    def normalized_params\n      ActionController::Parameters.new(\n         ActiveModelSerializers::Deserialization.jsonapi_parse(params)\n      )\n    end\nend\n```\n\nAnd the sessions serializer:\n\n``` ruby\nclass Api::V1::SessionSerializer \u003c Api::V1::BaseSerializer\n  type :session\n\n  attributes :email, :token, :user_id\n\n  has_one :user, serializer: Api::V1::UserSerializer do\n    link(:self) {api_v1_user_path(object.id)}\n    link(:related) {api_v1_user_path(object.id)}\n\n    object\n  end\n\n  def user\n    object\n  end\n\n  def user_id\n    object.id\n  end\n\n  def token\n    object.token\n  end\n\n  def email\n    object.email\n  end\nend\n\n```\n\nThe client probably needs only user's id, email and token but it's good to return some more data for better optimization.\nWe might save us from an extra request to the users endpoint :)\n\n\n```http\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"session\",\n    \"attributes\": {\n      \"email\": \"example@railstutorial.org\",\n      \"token\": \"f42f5ccee3689209e7ca8e4f9bd830e2\",\n      \"user-id\": 1\n    },\n    \"relationships\": {\n      \"user\": {\n        \"data\": {\n          \"id\": \"1\",\n          \"type\": \"users\"\n        },\n        \"links\": {\n          \"self\": \"/api/v1/users/1\",\n          \"related\": \"/api/v1/users/1\"\n        }\n      }\n    }\n  },\n  \"included\": [\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"name\": \"Example User\",\n        \"email\": \"example@railstutorial.org\",\n        \"created-at\": \"2016-11-05T10:15:26Z\",\n        \"updated-at\": \"2016-11-19T21:30:10Z\",\n        \"password-digest\": \"$2a$10$or7HFYm/H07/uE79wDae3uXMmHOX3BvRKdgedPJ1SPceiMA40V25O\",\n        \"remember-digest\": null,\n        \"admin\": true,\n        \"activation-digest\": \"$2a$10$X5IeDtGZPuZQEVQ.ZiUP4eUzfw9M9Pag/nR.0ONiXwAAp3w98iAuC\",\n        \"activated\": true,\n        \"activated-at\": \"2016-11-05T10:15:26.300Z\",\n        \"reset-digest\": null,\n        \"reset-sent-at\": null,\n        \"token\": \"f42f5ccee3689209e7ca8e4f9bd830e2\",\n        \"microposts-count\": 99,\n        \"followers-count\": 37,\n        \"followings-count\": 48,\n        \"following-state\": false,\n        \"follower-state\": false\n      },\n      \"relationships\": {\n        \"microposts\": {\n          \"links\": {\n            \"related\": \"/api/v1/microposts?user_id=1\"\n          }\n        },\n        \"followers\": {\n          \"links\": {\n            \"related\": \"/api/v1/users/1/followers\"\n          }\n        },\n        \"followings\": {\n          \"links\": {\n            \"related\": \"/api/v1/users/1/followings\"\n          }\n        }\n      }\n    }\n  ]\n}\n\n```\n_Tip: Yes we need to add proper authorization: return only the attributes that the client is allowed to see, we will deal with that a bit later :)_\n\n\nOnce the client has the token it sends both token and email to the API for each subsequent request.\nNow let's add the `authenticate_user!` filter inside the `Api::V1::BaseController`:\n\n``` ruby\n  def authenticate_user!\n      token, options = ActionController::HttpAuthentication::Token.token_and_options(\n        request\n      )\n\n      return nil unless token \u0026\u0026 options.is_a?(Hash)\n\n      user = User.find_by(email: options['email'])\n      if user \u0026\u0026 ActiveSupport::SecurityUtils.secure_compare(user.token, token)\n        @current_user = user\n      else\n        return UnauthenticatedError\n      end\n  end\n```\n`ActionController::HttpAuthentication::Token` parses Authorization header which holds the token.\nActually, an Authorization header looks like that:\n\n``` http\nAuthorization: Token email=myemail@email.com, token=\"f42f5ccee3689209e7ca8e4f9bd830e2\"\n```\n\nThe email is needed to avoid timming attacks (more info [here](https://github.com/vasilakisfil/rails5_api_tutorial/issues/11)).\n\nNow that we have set the `current_user` it's time to move on to authorization.\n\n\n## Adding Authorization\nFor authorization we will use [Pundit](https://github.com/elabs/pundit), a minimalistic yet wonderful gem based on policies.\nIt's worth mentioning that authorization should be the same regardless of the API version, so no namespacing here.\nThe original Rails app doesn't have an authorization gem but uses a custom one (nothing wrong with that!)\n\nAfter we add the gem and run the generators for default policy we create the user policy:\n\n``` ruby\nclass UserPolicy \u003c ApplicationPolicy\n  def show?\n    return true\n  end\n\n  def create?\n    return true\n  end\n\n  def update?\n    return true if user.admin?\n    return true if record.id == user.id\n  end\n\n  def destroy?\n    return true if user.admin?\n    return true if record.id == user.id\n  end\n\n  class Scope \u003c ApplicationPolicy::Scope\n    def resolve\n      scope.all\n    end\n  end\nend\n```\nThe problem with `Pundit` is that it has a black-white kind of policy.\nEither you are allowed to see the resource or not allowed at all.\nWe would like to have a mixed-policy (the grey one): you are allowed but only to specific resource attributes.\n\nIn our app we will have 3 roles:\n* a `Guest` who is asking API data without authenticating at all\n* a `Regular` user\n* an `Admin`, think it like God which has access to everything\n\nFor that we will use [FlexiblePermissions](https://github.com/vasilakisfil/flexible-permissions) a gem that works\non top of `Pundit`. Basically the idea is that apart from telling controller if this\nuser is allowed to have access or not, you also embed the type of access: which attributes\nthe user has access. You can also specify the defaults (which is a subset of the permitted attributes)\nif the user is not requesting specific fields. So, first let's specify the permission classes for `User` roles:\n\n```ruby\nclass UserPolicy \u003c ApplicationPolicy\n  class Admin \u003c FlexiblePermissions::Base\n    class Fields \u003c self::Fields\n      def permitted\n        super + [\n          :links\n        ]\n      end\n    end\n  end\n\n  class Regular \u003c Admin\n    class Fields \u003c self::Fields\n      def permitted\n        super - [\n          :activated, :activated_at, :activation_digest, :admin,\n          :password_digest, :remember_digest, :reset_digest, :reset_sent_at,\n          :token, :updated_at,\n        ]\n      end\n    end\n  end\n\n  class Guest \u003c Regular\n    class Fields \u003c self::Fields\n      def permitted\n        super - [:email]\n      end\n    end\n  end\nend\n```\n\nAs you can see `Admin` role (when requesting `User(s)`) has access to everything, plus, the links attributes,\nwhich is a computed property defined inside the Serializer.\n\nThen we have the `Regular` role (when requesting `User(s)`) which inherits from `Admin` but we chop some\nprivate attributes.\n\nThen from `Guest` role we remove even more attributes (namely, the user's email).\n\nHaving defined the roles, we can now define the authorization methods for `User` resource:\n\n```ruby\nclass UserPolicy \u003c ApplicationPolicy\n  def create?\n    return Regular.new(record)\n  end\n\n  def show?\n    return Guest.new(record) unless user\n    return Admin.new(record) if user.admin?\n    return Regular.new(record)\n  end\nend\n```\n\nThat's the classic CRUD of a resource. As you can see, for user creation we set\n`Regular` permissions no matter what.\nFor the rest actions though (here showing only `show` action), we alternate between roles depending on the\nuser. Let's see how our controller becomes now:\n\n```ruby\n\n  def show\n    auth_user = authorize_with_permissions(User.find(params[:id]))\n\n    render jsonapi: auth_user.record, serializer: Api::V1::UserSerializer,\n      fields: {user: auth_user.fields}\n  end\n```\nFrom the controller, we specify which attributes the serializer is allowed to return,\nbased on the `authorize_with_permissions`. So for a guest, the response becomes:\n\n```http\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"users\",\n    \"attributes\": {\n      \"name\": \"Example User\",\n      \"created-at\": \"2016-11-05T10:15:26Z\"\n    },\n    \"relationships\": {\n      \"microposts\": {\n        \"links\": {\n          \"related\": \"/api/v1/microposts?user_id=1\"\n        }\n      },\n      \"followers\": {\n        \"links\": {\n          \"related\": \"/api/v1/users/1/followers\"\n        }\n      },\n      \"followings\": {\n        \"links\": {\n          \"related\": \"/api/v1/users/1/followings\"\n        }\n      }\n    }\n  }\n}\n```\n\n\n## Adding pagination, rate limit and CORS\nPagination is necessary for 2 reasons.\nIt adds some very basic hypermedia for the front-end client and it increases the performance since it renders only a\nfraction of the total resources.\n\nFor pagination we will use the same gem that Michael is already using: [will_paginate](https://github.com/mislav/will_paginate).\nwe will only need to use it in the following 2 methods:\n\n``` ruby\n  def paginate(resource)\n    resource = resource.page(params[:page] || 1)\n    if params[:per_page]\n      resource = resource.per_page(params[:per_page])\n    end\n\n    return resource\n  end\n\n  #expects paginated resource!\n  def meta_attributes(object)\n    {\n      current_page: object.current_page,\n      next_page: object.next_page,\n      prev_page: object.previous_page,\n      total_pages: object.total_pages,\n      total_count: object.total_entries\n    }\n  end\n```\n\nI should note that you can also use [Kaminari](https://github.com/amatsuda/kaminari), they are almost identical.\n\nRate limit is a good way to filter unwanted bots or users that abuse our API.\nIt's implemented by [redis-throttle](https://github.com/andreareginato/redis-throttle)\ngem and as the name suggests it uses redis to store the limits based on the user's IP.\nWe only need to add the gem and add a couple of lines lines in a new file in `config/rack_attack.rb`\n\n``` ruby\nclass Rack::Attack\n  redis = ENV['REDISTOGO_URL'] || 'localhost'\n  Rack::Attack.cache.store = ActiveSupport::Cache::RedisStore.new(redis)\n\n  throttle('req/ip', limit: 1000, period: 10.minutes) do |req|\n    req.ip if req.path.starts_with?('/api/v1')\n  end\nend\n```\n\nand enable it in `config/application.rb`:\n\n``` ruby\n  config.middleware.use Rack::Attack\n```\n\n\n[CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) is a specification that \"that enables many resources\n(e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain from which the\nresource originated.\nEssentially it allows us to have loaded the javascript client in another domain from our API and allow the js to send\nAJAX requests to our API.\n\nFor Rails all we have to do is to install the `rack-cors` gem and allow:\n\n``` ruby\n    config.middleware.insert_before 0, \"Rack::Cors\" do\n      allow do\n        origins '*'\n        resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]\n      end\n    end\n```\n\nWe allow access from anywhere, as a proper API.\nWe can set restrictions on which clients are allowed to access the API by specifying the hostnames in origins.\n\n## Tests\nNow let's go and write some tests! We will use `Rack::Test` helper methods as described\n[here](https://gist.github.com/alex-zige/5795358).\nWhen building APIs it's important to test that the path input -\u003e controller -\u003e model -\u003e controller -\u003e serializer -\u003e\noutput works ok.\nThat's why I feel API tests stand between unit tests and integration tests.\nNote that since Michael has already added some model tests we don't have to be pedantic about it. We can skip models, and test\nonly API controllers.\n\n``` ruby\ndescribe Api::V1::UsersController, type: :api do\n  context :show do\n    before do\n      create_and_sign_in_user\n      @user = FactoryGirl.create(:user)\n\n      get api_v1_user_path(@user.id), format: :json\n    end\n\n    it 'returns the correct status' do\n      expect(last_response.status).to eql(200)\n    end\n\n    it 'returns the data in the body' do\n      body = JSON.parse(last_response.body, symbolize_names: true)\n      expect(body.dig(:data, :attributes, :name).to eql(@user.name)\n      expect(body.dig(:data, :attributes, :email).to eql(@user.name)\n      expect(body.dig(:data, :attributes, :admin).to eql(@user.admin)\n      expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.created_at.iso8601)\n      expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.updated_at.iso8601)\n    end\n  end\nend\n```\n\n`create_and_sign_in_user` method comes from our authentication helper:\n\n``` ruby\nmodule AuthenticationHelper\n  def sign_in(user)\n    header('Authorization', \"Token token=\\\"#{user.token}\\\"\")\n  end\n\n  def create_and_sign_in_user\n    user = FactoryGirl.create(:user)\n    sign_in(user)\n    return user\n  end\n  alias_method :create_and_sign_in_another_user, :create_and_sign_in_user\n\n  def create_and_sign_in_admin\n    admin = FactoryGirl.create(:admin)\n    sign_in(admin)\n    return admin\n  end\n  alias_method :create_and_sign_in_admin_user, :create_and_sign_in_admin\nend\n\nRSpec.configure do |config|\n  config.include AuthenticationHelper, type: :api\nend\n```\n\nWhat do we want to test?\n\n* the path input -\u003e controller -\u003e model -\u003e controller -\u003e serializer -\u003e output actually works ok\n* controller returns the correct error statuses\n* controller responds to the API attributes based on the user role that makes the request\n\nWhat we are actually doing here is that I re-implement the RSpecs methods [respond_to](https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/respond-to-matcher)\nand rspec-rails' [be_valid](http://www.rubydoc.info/gems/rspec-rails/RSpec/Rails/Matchers#be_valid-instance_method)\nmethod at a higher level.\nHowever, asserting each attribute of the API response to be equal with our initial\nobject takes too much time and space. And what if I change my serializer and use HAL or JSONAPI instead?\n\nInstead, we can use [rspec-api_helpers](https://github.com/kollegorna/rspec-api_helpers) which automate this process:\n\n```ruby\n\nrequire 'rails_helper'\n\ndescribe Api::V1::UsersController, type: :api do\n  context :show do\n    before do\n      create_and_sign_in_user\n      FactoryGirl.create(:user)\n      @user = User.last!\n\n      get api_v1_user_path(@user.id)\n    end\n\n    it_returns_status(200)\n    it_returns_attribute_values(\n      resource: 'user', model: proc{@user}, attrs: [\n        :id, :name, :created_at, :microposts_count, :followers_count,\n        :followings_count\n      ],\n      modifiers: {\n        created_at: proc{|i| i.in_time_zone('UTC').iso8601.to_s},\n        id: proc{|i| i.to_s}\n      }\n    )\n  end\nend\n```\n\nThis gem adds an automated way to test your JSONAPI (or any other API spec) respone\nby proviging you a simple API to test all attributes.\n\nFurthermore, to have more robust tests, we can add [rspec-json_schema](https://github.com/blazed/rspec-json_schema) that\ntests if the response follows a pre-defined JSON schema.\nFor instance, the JSON schema for regular role, is the following:\n\n```\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"data\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"attributes\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\"\n            },\n            \"email\": {\n              \"type\": \"string\"\n            },\n            \"created-at\": {\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\n            \"name\",\n            \"email\",\n            \"created-at\",\n          ]\n        },\n        \"relationships\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"microposts\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"links\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"related\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"related\"\n                  ]\n                }\n              },\n              \"required\": [\n                \"links\"\n              ]\n            },\n            \"followers\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"links\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"related\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"related\"\n                  ]\n                }\n              },\n              \"required\": [\n                \"links\"\n              ]\n            },\n            \"followings\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"links\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"related\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"related\"\n                  ]\n                }\n              },\n              \"required\": [\n                \"links\"\n              ]\n            }\n          },\n          \"required\": [\n            \"microposts\",\n            \"followers\",\n            \"followings\"\n          ]\n        }\n      },\n      \"required\": [\n        \"id\",\n        \"type\",\n        \"attributes\",\n        \"relationships\"\n      ]\n    }\n  },\n  \"required\": [\n    \"data\"\n  ]\n}\n```\n\nNotice on the `required` and `additionalProperties` properties which tighten the schema a lot.\nEventually the test spec for `show` action becomes:\n\n```ruby\nrequire 'rails_helper'\n\ndescribe Api::V1::UsersController, '#show', type: :api do\n  describe 'Authorization' do\n    context 'when as guest' do\n      before do\n        FactoryGirl.create(:user)\n        @user = User.last!\n\n        get api_v1_user_path(@user.id)\n      end\n\n      it_returns_status(401)\n      it_follows_json_schema('errors')\n    end\n\n    context 'when authenticated as a regular user' do\n      before do\n        create_and_sign_in_user\n        FactoryGirl.create(:user)\n        @user = User.last!\n\n        get api_v1_user_path(@user.id)\n      end\n\n      it_returns_status(200)\n      it_follows_json_schema('regular/user')\n      it_returns_attribute_values(\n        resource: 'user', model: proc{@user}, attrs: [\n          :id, :name, :created_at, :microposts_count, :followers_count,\n          :followings_count\n        ],\n        modifiers: {\n          created_at: proc{|i| i.in_time_zone('UTC').iso8601.to_s},\n          id: proc{|i| i.to_s}\n        }\n      )\n    end\n\n    context 'when authenticated as an admin' do\n      before do\n        create_and_sign_in_admin\n        FactoryGirl.create(:user)\n        @user = User.last!\n\n        get api_v1_user_path(@user.id)\n      end\n\n      it_returns_status(200)\n      it_follows_json_schema('admin/user')\n      it_returns_attribute_values(\n        resource: 'user', model: proc{@user}, attrs: User.column_names,\n        modifiers: {\n          [:created_at, :updated_at] =\u003e proc{|i| i.in_time_zone('UTC').iso8601.to_s},\n          id: proc{|i| i.to_s}\n        }\n      )\n    end\n  end\nend\n```\n\n\nGiven that JSON schemas can be very verbose and specific regarding the response\nattributes I feel all these techniques combined can give us very powerful tests.\n\n\n\n## Final API\nAs you might noticed, we have skipped some stuff like creating or updating a user.\nThat was intentional as I didn't want to overload you with information.\nYou can dig in the code and see how everything is implemented :)\n\nJust for reference, this API is used for the [Ember app](https://github.com/vasilakisfil/ember_on_rails5)\nthat imitates [Rails Tutorial app](https://www.railstutorial.org/).\n\nFor authentication and authorization in the ember side we used the\n[ember-simple-auth](https://github.com/simplabs/ember-simple-auth) addon\nalthough we haven't used devise in Rails app.\nBut that's the beauty of APIs: you can hide your implementation details :)\n\nIn the following sections I highlight some important aspects you should take into account\nwhen building APIs.\nAll of them (except UUIDs and model caching) have been implemented in the final API that you will find\nin the github repo.\nI really think you should take it a drive and try to add model caching (I would\nsuggests shopify's [identity_cache](https://github.com/Shopify/identity_cache)) if you wanna scale :)\n\n## Bonus: Some Optimizations and tips\n### UTC Timestamps\nWhen our resource includes a day, it's good to have it in UTC time and iso8601 format.\nIn general, we really don't want to include anywhere timezones in our API.\nIf we clearly state that our datetimes are in utc and we only accept utc datetime, clients are responsible to convert\nthe utc datetime to their local datetime (for instance, in Ember this is very easy using [moment](http://momentjs.com/)\nand [transforms](http://emberjs.com/api/data/classes/DS.Transform.html)).\n\n### Counters\nAnother thing is that when building an API we should always think from the client perspective.\nFor instance if the client requests a user, it will probably like to know the number\nof microposts, followers or followings (users the user follows) that user has.\n\nAt the moment, this can be achieved by sending an extra request to each one of those\nresources and check the `total_count` of the meta in the response.\nHaving the client sending more requests is not good for the client, it's not good\nfor us either since this means more requests to our API.\n\nInstead we can add (cache) counters to each of the associations and return those\nalong with the user information.\nTo achieve that, we first need to create a column for each counter and then tell\nrails to cache the counters (by adding `counter_cache: assocation_count` in\neach association). Here we go:\n\nFirst we create a migration:\n```ruby\nclass AddCacheCounters \u003c ActiveRecord::Migration[5.0]\n  def change\n    add_column :users, :microposts_count, :integer, null: false, default: 0\n    add_column :users, :followers_count, :integer, null: false, default: 0\n    add_column :users, :followings_count, :integer, null: false, default: 0\n  end\nend\n```\nThen inside `Micropost` model:\n\n```ruby\n  belongs_to :user, counter_cache: true\n```\n\nand inside `Relationship` model:\n\n```ruby\n  belongs_to :follower, class_name: \"User\", counter_cache: :followings_count\n  belongs_to :followed, class_name: \"User\", counter_cache: :followers_count\n```\n\nI should note that in regular Rails development these counter cache columns are\nadded even when not having an API.\nIt helps a lot to cache them in a database column instead of running the `SQL COUNT(*)`\neach time we need it.\n\n### Follower/Following states\nOk let's thing from the client perspective again.\nLet's say that the client wants to retrieve a user, so it gets the user information\nalong with the counters.\nHowever, in most cases you will want to know whether you follow this user or not\nand whether this user follows you or not.\n\nIn a regular Rails app we can do instantly (even from the view) the query, or use\na helper and figure it out.\nHere we need to take a different approach instead.\nIt will cost us much less if we give this information beforehand instead\nof creating a new endpoint just for that and letting the client do the request.\n\nWe will add these states in the serializers as computed properties:\n\n```ruby\n  attribute :following_state\n  attribute :follower_state\n\n  def following_state\n    Relationship.where(\n      follower_id: current_user.id,\n      followed_id: object.id\n    ).exists?\n  end\n\n  def follower_state\n    Relationship.where(\n      follower_id: object.id,\n      followed_id: current_user.id\n    ).exists?\n  end\n```\nWe should cache this information (but I leave it up to you how to do it :) )\n\nEven if you feel that this information is rarely used by clients, you should still\nhave it in the user resource but instead of providing these resource attributes\n_by default_, you can provide them only when the user specifies a JSONAPI `fields`\nparam.\nWhich brings us to the next topic: help the client by building a modern API. Remember\nthat you don't build the API for yourself but for the clients.\nThe better the API for the clients, the more API clients you will have :)\n\n## Bonus: Build a modern API\nA modern API, regadless the spec you use, should have _at least_ the following attributes:\n\n1. Sparse fields\n2. Granular permissions\n3. Associations on demand\n4. Defaults (help the client!)\n5. Sorting \u0026 pagination\n6. Filtering collections\n7. Aggregation queries\n\nThese API attributes will help the client to avoid unessecary data and ask for\nexactly what is needed helping us too (since we won't compute unused data).\nIdeally we would like to give to the client an ORM on top of HTTP.\n\n### Sparse fields, Granular permissions and Associations on demand\nWe have already solved the problem of granular permissions by using [flexible_permissions](https://github.com/vasilakisfil/flexible-permissions) roles.\nEach role is allowed only specific attributes and associations.\nAlso the same gems allows us to select only a subset of the allowed fields.\n\nJSONAPI already specified how a client can ask specific fields/associations of a resource.\nWhat we need to do now is to link the user's asked fields/associaions with role's\npermitted attributes and associations.\n\n### Defaults, Sorting \u0026 pagination, Filtering collections and Aggregation queries\nWe have already set the defaults using [flexible_permissions](https://github.com/vasilakisfil/flexible-permissions).\nWe have also added pagination in our response.\n\nNow we need to allow the client to ask for specific sorting, filtering collections by\nsending custom queries and ask for aggregated data (for instance the average number of\nfollowers of a user).\n\nFor those things we are going to use [active_hash_relation](https://github.com/kollegorna/active_hash_relation) gem which adds a\nwhole API in our index method for free! Be sure to [check it out](https://github.com/kollegorna/active_hash_relation#the-api)!\nIt's as simple as adding 2 lines:\n\n``` ruby\nclass Api::V1::UsersController \u003c Api::V1::BaseController\n  include ActiveHashRelation\n\n  def index\n    auth_users = policy_scope(@users)\n\n    render jsonapi: auth_users.collection,\n      each_serializer: Api::V1::UserSerializer,\n      fields: {user: auth_users.fields},\n      meta: meta_attributes(auth_users.collection)\n  end\nend\n```\n\nNow, using ActiveHashRelation API we can ask for users that were created after a specific date or users with a specific\nemail prefix etc. We can also ask for specific sorting and aggregation queries.\n\n_However, it's a good idea in terms of performance and security to first filter the permitted params_\n\n\n\n## Bonus: Adding automatic deployment\nA new Rails project without automatic deployment is not cool.\nServices like [travis](https://travis-ci.org/), [circleci](https://circleci.com/) and [codeship](https://codeship.com) help us build and deploy faster.\nIn this project we will use [codeship](https://codeship.com/).\n\nOnce we create a new project we we need to add the following commands on setup section:\n```\nrvm use 2.3.3\nbundle install\nbundle exec rake db:create\nbundle exec rake db:migrate\n```\nIn test secion we can run all tests (both Michael's and API tests):\n\n```\nrake test\nbundle exec rspec spec\n```\n\nThen we need to create a heroku app (if heroku is what we want for code hosting) and\nget the API key (I am surprised that heroku doesn't provide any permission listing for its API keys :/)\nwhich is required by Codeship (or any other automatic deployment service) to deploy the code.\nOnce we have it we add a heroku pipeline and we are ready.\n\n\nNow If we commit to master and our tests are green, it will push and deploy our repo in heroku and run migrations :)\n\n## Bonus: In case of a break change: how to handle Version 2\nWe build our API, we ship it and everything works as expected.\nWe can always add more endpoints or enhance current ones and keep our current version as long as we don't have a\nbreaking changes. However, although rare, we might reach the point where we must have a break change because the\nrequirements changed.\nDon't panic!\nAll we have to do is define the same routes but for V2 namespace, define the V2 controllers that inherit from V1\ncontrollers and override any method we want.\n\n``` ruby\nclass Api::V2::UsersController \u003c Api::V1::UsersController\n\n  def index\n    #new overriden index here\n  end\n\nend\n```\n\nIn that way we save a lot of time and effort for our V2 API ( although for shifting an API version you will probably\nwant more changes than a single endpoint).\n\n## Bonus: Add documentation!\nDocumenting our API is vital even if it supports hypermedia.\nDocumentation helps users to speed up their app or client development.\nThere are many documentation tools for rails like [swagger](http://swagger.io/)\n and [slate](https://github.com/tripit/slate).\n\nHere we will use [slate](https://github.com/tripit/slate) as it is easier to start with.\n\nOur app is rather small and we are going to have docs in the same repo with the\nrails app but in larger APIs we might want them in a separate repository because\nit generates css and html files which are also versioned and there is no point since\nthey are generated with a bundler command.\n\nCreate an `app/docs/` directory and clone the slate repository there and delete\nthe .git directory (we don't need slate revisions).\nIn a `app/docs/config.rb` set the build directory to public folder:\n\n``` ruby\nset :build_dir, '../public/docs/'\n```\n\nand start writing your docs.\nYou can take some inspiration from [our docs](https://rails-tutorial-api.herokuapp.com/docs/) :)\n\n## Bonus: Looking ahead\nAs I mentioned there are 2 things that haven't implemented, but you should try to implement them as a test :)\n\nFirst, it's a good idea is to use uuids instead of ids when we know that our app is going to have an API.\nWith ids we might unveil sensitive information to an attacker.\nThere is a slight performance hit on database when using UUIDs but probably the benefits are greater.\nYou can also check [this](https://www.clever-cloud.com/blog/engineering/2015/05/20/why-auto-increment-is-a-terrible-idea/) blog post for more information.\n\nSecondly we haven't added any caching. In my experience a Rails app _like that_  should stand\naround 1000 req/minute in a regular heroku dyno X2 (3 puma processes each having 2 workers,\neach having ~10 threads giving us in total 60 fronts) but adding cache should take it to 2500.\nHowever I haven't tested that. Is anyone interested to tell me how much he/she manage to\nreach? (with or without cache). I would be happy to add an extra sections just for optimizations\nfrom you folks. Just create a PR :D\n\n\n## That's all folks\nThat's all for now. You should really start building your Rails API _today_ and not tomorrow.\n\nI am now going to prepare the [Ember](https://github.com/vasilakisfil/ember_on_rails5) tutorial. Until then take care and have fun!\n\n_Did you know that you can contribute to this tutorial by opening an issue or even sending a pull request?_\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvasilakisfil%2Frails5_api_tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvasilakisfil%2Frails5_api_tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvasilakisfil%2Frails5_api_tutorial/lists"}