百科.dev
全部条目AI 编程趋势榜开源项目技术资讯提交条目
登录
< 返回工具列表
R

rails5_api_tutorial

> 后端框架
开源

了解如何在 Michael Hartl 的 Rails 5 教程中构建现代 API

449 stars0 点赞1 次浏览
访问官网GitHub

工具介绍

了解如何在 Michael Hartl 的 Rails 5 教程中构建现代 API

Build an API in your Rails app now! (Rails 5 version)

Note 1: If you are looking for the regular readme, it's here.

Note 2: You can contribute to this tutorial by opening an issue or even sending a pull request!

Note 3: With the API I built, I went on and created the same app in Ember.

I will show how you can extend your Rails app and build an API without changing a single line of code from your existing app. We will be using Michael Hartl's Rails tutorial (I actually started learning Rails and subsequently Ruby from that tutorial, I really owe a beer to that guy) which is a classic Rails app and extend it by building an API for the app.

Designing our API

Designing an API is not an easy process. Usually it's very difficult to know beforehand what the client will need. However we will make our best to support most clients needs:

  • have a resty approach using the popular JSONAPI spec
  • use hypermedia for related resources instead of embedding them
  • have in the same response data that otherwise would require many requests in the client

By the way, there is a long discussion about what REST means. Is just JSONAPI as REST as Joy Fielding's defined it? Definitely not. However, it's more resty than regular JSON response, plus it has a wide support in terms of libraries.

Moving forward, let's add our first resource, let it be a user. But before adding the controller let's add the routes first:

  #api
  namespace :api do
    namespace :v1 do
      resources :sessions, only: [:create, :show]
      resources :users, only: [:index, :create, :show, :update, :destroy] do
        post :activate, on: :collection
        resources :followers, only: [:index, :destroy]
        resources :followings, only: [:index, :destroy] do
          post :create, on: :member
        end
        resource :feed, only: [:show]
      end
      resources :microposts, only: [:index, :create, :show, :update, :destroy]
    end
  end

All REST routes for each record, only GET method for collections (Rails muddles up collection REST routes with element REST routes in the same controllers) and a couple custom routes.

As you can see we have many routes. The idea is that the tutorial will mostly touch and show you a couple of them and you will manage to understand and see the rest from the code inside the repo. I think extended tutorials are boring :). However, if you find something weird or you don't understand something you are always welcomed to open an issue and ask :)

Let's create the users API controller and add support for the GET method on a single record:

Adding our first API resource

The first thing we need to do is to separate our API from the rest of the app. In order to do that we will create a new Controller under a different namespace. Given that it's good to have versioned API let's go and create our first controller under app/controllers/api/v1/

class Api::V1::BaseController  hex).any?
    end
  end

and exactly after that we create the migration:

class AddTokenToUsers  controller -> model -> controller -> serializer ->
output works ok.
That's why I feel API tests stand between unit tests and integration tests.
Note that since Michael has already added some model tests we don't have to be pedantic about it. We can skip models, and test
only API controllers.

``` ruby
describe Api::V1::UsersController, type: :api do
  context :show do
    before do
      create_and_sign_in_user
      @user = FactoryGirl.create(:user)

      get api_v1_user_path(@user.id), format: :json
    end

    it 'returns the correct status' do
      expect(last_response.status).to eql(200)
    end

    it 'returns the data in the body' do
      body = JSON.parse(last_response.body, symbolize_names: true)
      expect(body.dig(:data, :attributes, :name).to eql(@user.name)
      expect(body.dig(:data, :attributes, :email).to eql(@user.name)
      expect(body.dig(:data, :attributes, :admin).to eql(@user.admin)
      expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.created_at.iso8601)
      expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.updated_at.iso8601)
    end
  end
end

create_and_sign_in_user method comes from our authentication helper:

module AuthenticationHelper
  def sign_in(user)
    header('Authorization', "Token token=\"#{user.token}\"")
  end

  def create_and_sign_in_user
    user = FactoryGirl.create(:user)
    sign_in(user)
    return user
  end
  alias_method :create_and_sign_in_another_user, :create_and_sign_in_user

  def create_and_sign_in_admin
    admin = FactoryGirl.create(:admin)
    sign_in(admin)
    return admin
  end
  alias_method :create_and_sign_in_admin_user, :create_and_sign_in_admin
end

RSpec.configure do |config|
  config.include AuthenticationHelper, type: :api
end

…

ruby

require 'rails_helper'

describe Api::V1::UsersController, type: :api do
  context :show do
    before do
      create_and_sign_in_user
      FactoryGirl.create(:user)
      @user = User.last!

      get api_v1_user_path(@user.id)
    end

    it_returns_status(200)
    it_returns_attribute_values(
      resource: 'user', model: proc{@user}, attrs: [
        :id, :name, :created_at, :microposts_count, :followers_count,
        :followings_count
      ],
      modifiers: {
        created_at: proc{|i| i.in_time_zone('UTC').iso8601.to_s},
        id: proc{|i| i.to_s}
      }
    )
  end
end

This gem adds an automated way to test your JSONAPI (or any other API spec) respone by proviging you a simple API to test all attributes.

Furthermore, to have more robust tests, we can add rspec-json_schema that tests if the response follows a pre-defined JSON schema. For instance, the JSON schema for regular role, is the following:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "data": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "type": {
          "type": "string"
        },
        "attributes": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string"
            },
            "email": {
              "type": "string"
            },
            "created-at": {
              "type": "string"
            }
          },
          "required": [
            "name",
            "email",
            "created-at",
          ]
        },
        "relationships": {
          "type": "object",
          "properties": {
            "microposts": {
              "type": "object",
              "properties": {
                "links": {
                  "type": "object",
                  "properties": {
                    "related": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "related"
                  ]
                }
              },
              "required": [
                "links"
              ]
            },
            "followers": {
              "type": "object",
              "properties": {
                "links": {
                  "type": "object",
                  "properties": {
                    "related": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "related"
                  ]
                }
              },
              "required": [
                "links"
              ]
            },
            "followings": {
              "type": "object",
              "properties": {
                "links": {
                  "type": "object",
                  "properties": {
                    "related": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "related"
                  ]
                }
              },
              "required": [
                "links"
              ]
            }
          },
          "required": [
            "microposts",
            "followers",
            "followings"
          ]
        }
      },
      "required": [
        "id",
        "type",
        "attributes",
        "relationships"
      ]
    }
  },
  "required": [
    "data"
  ]
}

Notice on the required and additionalProperties properties which tighten the schema a lot. Eventually the test spec for show action becomes:

require 'rails_helper'

describe Api::V1::UsersController, '#show', type: :api do
  describe 'Authorization' do
    context 'when as guest' do
      before do
        FactoryGirl.create(:user)
        @user = User.last!

        get api_v1_user_path(@user.id)
      end

      it_returns_status(401)
      it_follows_json_schema('errors')
    end

    context 'when authenticated as a regular user' do
      before do
        create_and_sign_in_user
        FactoryGirl.create(:user)
        @user = User.last!

        get api_v1_user_path(@user.id)
      end

      it_returns_status(200)
      it_follows_json_schema('regular/user')
      it_returns_attribute_values(
        resource: 'user', model: proc{@user}, attrs: [
          :id, :name, :created_at, :microposts_count, :followers_count,
          :followings_count
        ],
        modifiers: {
          created_at: proc{|i| i.in_time_zone('UTC').iso8601.to_s},
          id: proc{|i| i.to_s}
        }
      )
    end

    context 'when authenticated as an admin' do
      before do
        create_and_sign_in_admin
        FactoryGirl.create(:user)
        @user = User.last!

        get api_v1_user_path(@user.id)
      end

      it_returns_status(200)
      it_follows_json_schema('admin/user')
      it_returns_attribute_values(
        resource: 'user', model: proc{@user}, attrs: User.column_names,
        modifiers: {
          [:created_at, :updated_at] => proc{|i| i.in_time_zone('UTC').iso8601.to_s},
          id: proc{|i| i.to_s}
        }
      )
    end
  end
end

…

ruby
class AddCacheCounters < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :microposts_count, :integer, null: false, default: 0
    add_column :users, :followers_count, :integer, null: false, default: 0
    add_column :users, :followings_count, :integer, null: false, default: 0
  end
end

Then inside Micropost model:

  belongs_to :user, counter_cache: true

and inside Relationship model:

  belongs_to :follower, class_name: "User", counter_cache: :followings_count
  belongs_to :followed, class_name: "User", counter_cache: :followers_count

…

ruby
  attribute :following_state
  attribute :follower_state

  def following_state
    Relationship.where(
      follower_id: current_user.id,
      followed_id: object.id
    ).exists?
  end

  def follower_state
    Relationship.where(
      follower_id: object.id,
      followed_id: current_user.id
    ).exists?
  end

…

 ruby
class Api::V1::UsersController < Api::V1::BaseController
  include ActiveHashRelation

  def index
    auth_users = policy_scope(@users)

    render jsonapi: auth_users.collection,
      each_serializer: Api::V1::UserSerializer,
      fields: {user: auth_users.fields},
      meta: meta_attributes(auth_users.collection)
  end
end

…


rvm use 2.3.3
bundle install
bundle exec rake db:create
bundle exec rake db:migrate

In test secion we can run all tests (both Michael's and API tests):

rake test
bundle exec rspec spec

…

 ruby
class Api::V2::UsersController < Api::V1::UsersController

  def index
    #new overriden index here
  end

end

…

 ruby
set :build_dir, '../public/docs/'

and start writing your docs. You can take

Issues· 1 开放

查看全部 Issues在 GitHub 打开

暂无开放 Issues,或尚未同步最近议题。

> 标签

Rubyapirailstutorial

暂无评论,来聊聊你的看法吧

> 工具信息

发布日期2026年8月1日
最后更新2026年9月17日
分类后端框架
定价开源

> 相关工具

N
Node.js
基于 V8 的 JavaScript 运行时
D
Django
Python 高级 Web 框架
S
Spring Boot
Java 生态主流微服务框架