[RUBY] Introducing the settings and reference articles that are first done in Rails new development

motivation My name is yoshixj. Recently I started writing Golang. I'm going to flirt with Rails. December 2020 If you develop a new API with personal Rails, I will roughly write the flow of doing it like this. The author's experience is rough, Rails experience is about 3-4 years, he has a lot of experience in new development at startup, and he has little operational experience.

The content is better if people who started to use Rails for the time being should do it around here. This article is like a summary of what I'm glad to have implemented by googled, as I haven't had much experience in the last few years. I think there are some long and redundant parts, but it's soft.

agenda --Create git repository --Install Gemfile --docker settings

Create git repository

This time, we will start with a repository already on github. When creating rails, git is automatically set by rails new, but git is set before rails new.

$ git pull repository_name
$ cd repository_name
$ ls -al

drwxr-xr-x  14 yoshikimasubuchi  staff       448  5  2 11:08 .git

docker settings

I will set up Docker. I will do it with docker-compose from rails new. I refer to this area. Rails new senior recommended procedure

The directory structure is up to the team.

In my experience, When you turn off docker-compose with rails alone, it looks like this.

--repository_name
  ├── Gemfile
  ├── docker-compose.yml
  ├── app
  ├── docker
      ├── api
           ├── Dockerfile

The contents of the Dockefile look like this

FROM ruby:2.7.1
ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

RUN apt-get update -qq && apt-get install -y \
    build-essential \
    nodejs \
 && rm -rf /var/lib/apt/lists/*

ENV ENTRYKIT_VERSION 0.4.0
RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && mv entrykit /bin/entrykit \
  && chmod +x /bin/entrykit \
  && entrykit --symlink

WORKDIR /usr/src/app
COPY . /usr/src/app

RUN gem install bundler

ENTRYPOINT [ \
    "prehook", "ruby -v", "--", \
    "prehook", "bundle install -j3 --quiet", "--"]
version: '3'
services:
  db:
    image: postgres:11.3
    ports:
      - "5432:5432"
    volumes:
      - db:/var/lib/postgresql/data:z
  api:
    build:
      context: .
      dockerfile: ./docker/api
    volumes:
      - .:/usr/src/app
    command: /bin/sh -c "rm -f /usr/src/app/tmp/pids/server.pid && bundle exec rails s -p 8080 -b '0.0.0.0'"
    ports:
      - "8080:8080"
    depends_on:
      - db
    env_file:
      - rails.env
    environment:
      - DB_USER=postgres
      - DB_PASS=
      - DB_HOST=db
      - DB_PORT=5432
      # for ruby 2.6 alart https://k-koh.hatenablog.com/entry/2020/02/07/145957
      # - RUBYOPT='-W:no-deprecated -W:no-experimental'
    tty: true
    stdin_open: true

volumes:
   db:

Install Gemfile install rails

Set up a Gemfile to install Rails.

$ docker-compose build
$ docker-compose run api bundle init

Then, Gemfile will be installed. Add gem'rails' to the installed Gemfile.

 frozen_string_literal: true

source 'https://rubygems.org'

 git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

gem 'rails'

After adding, bundle.

$ docker-compose run api bundle install --path vendor/bundle

rails has been installed!

It may not be relevant in the docker environment, but I will add the bundle settings as well.

---
BUNDLE_PATH: vendor/bundle
BUNDLE_JOBS: 4
BUNDLE_DISABLE_SHARED_GEMS: '1'

rails new Finally rails new. This time we will do rails new in API mode. This time we're using rspec for our testing framework, so we'll do --skip-test. You can skip active-strage etc. other than the following, but I will not skip it because I may use it.

$ docker-compose run api bundle exec rails new . --api -d=postgresql --skip-git --skip-test --api --skip-bundle

Once you have an app with rails new, write a Gemfile. For the time being, I put it in development first.

 frozen_string_literal: true

source 'https://rubygems.org'

 git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

ruby '2.7.1'

gem 'rails'

gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.4.2', require: false
gem 'rack-cors'
gem 'pg'

group :development, :test do
  gem 'brakeman' # for security
  gem 'dotenv-rails'
  gem 'guard-rspec', require: false
  gem 'rspec-benchmark'
  gem 'rspec-rails'
  gem 'rspec_junit_formatter'
  gem 'rubocop'
  gem 'factory_bot_rails'
  gem 'faker'
  gem 'faker-japanese'
  gem 'pry-rails'
  gem 'pry-byebug'
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'hirb'
end
$ docker-compose run api bundle

I will also write the DB settings.

default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['DB_USER'] %>
  password: <%= ENV['DB_PASS'] %>
  host: <%= ENV.fetch('DB_HOST') { 'localhost' } %>
  port: <%= ENV.fetch('DB_PORT') { 5432 } %>

development:
  <<: *default
  database: earlybird-api_development

test:
  <<: *default
  database: earlybird-api_test
production:
  <<: *default
  database: earlybird-api_production
  username: earlybird-api
  password: <%= ENV['earlybird-api_DATABASE_PASSWORD'] %>

The setting of the default part matches the description of docker-compose.yml.

$ docker-compose run api bundle exec rails db:create

Now you can create an API with rails!

rubocop settings

It is important to properly configure rubocop in team development. If there is no ruby ​​project in your company and there is no rule on ruby ​​cop settings, set it like this.

rubocop-rails

.rubocop.yml


require: rubocop-rails

Rails:
  Enabled: true

AllCops:
  TargetRubyVersion: 2.7.3
  TargetRailsVersion: 6.0.2
  Exclude:
    - bin/*
    - db/**/*
    - vendor/**/*
    - test/**/*
    - config/**/*
    - lib/**/*
    - Gemfile
    - db/schema.rb

rspec install

Install rspec. If you forget to install it first, the model specs and controller specs will not be created automatically, which will be troublesome.

$ docker-compose run api bundle exec rails generate rspec:install

install factry bot

If you forget to install the factory bot as well, the file will not be created at the same time as the model is created, so it will be troublesome.

 RSpec.configure do |config|
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.include FactoryBot::Syntax::Methods # add

timezone setting

--ruby process timezone

Set. Rails has a complicated Timezone, so I set each setting first. Reference: Organize Rails and surrounding TimeZone settings (active_record.default_timezone trap)

Rails Time Zone Summary

This time, we are assuming a product that will be used only in Japan. If you have overseas expansion, I think it's a good idea to arrange all in UTC.

ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

config/application.rb


config.time_zone = 'Tokyo'
config.active_record.default_timezone = :local
set time zone 'Asia/Tokyo';

-- postgresql.timezone in conf='Japan'Also listed

Try to use TimeWithZone when dealing with time in your coding.

CORS settings

Set CROS. If you don't do this, you won't be able to hit the api from the front application with a cross origin error.

require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
# require "sprockets/railtie"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module App
  class Application < Rails::Application
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
    config.active_record.time_zone_aware_types = [:datetime, :time]

    config.i18n.default_locale = :ja
    config.api_only = true
    #Add the following
    config.middleware.use Rack::Cors do
      allow do
        origins ENV['CORS_ALLOWED_ORIGINS']
        resource "*",
                 headers: :any,
                 methods: [:get, :post, :put, :patch, :delete],
                 expose: []
      end
    end
  end
end

At this point, I think you've set up a working Rails app. From the following, we will add the settings in consideration of operation.

Unified error response

Make settings to unify the error response. I used to rescue the response in ApplicationController, but apparently it's better to use Rails'exception_app. I haven't actually used this, but I think it's probably set as follows.

Reference: I summarized the exception handling and error page display of Rails application

# app/controllers/errors_controller.rb
class ErrorsController < ActionController::Base
  rescue_from StandardError, with: :render_500
  rescue_from ActiveRecord::RecordNotFound, with: :render_404
  rescue_from ActiveRecord::RecordInvalid, with: :render_400

  def show
    raise env['action_dispatch.exception']
  end

  def render_400(err)
    render_error(err, 400)
  end

  def render_404(err)
    render_error(err, 404)
  end

  def render_500(err)
    render_error(err, 404)
  end

  def render_error(err, status)
      render json: ErrorSerializer.new(errr), status: status
    end
  end
end

# config/initializers/exceptions_app.rb
Rails.application.configure do
  config.exceptions_app = ErrorsController.action(:show)
end

Unification of guidelines

When developing an API, first decide on guidelines such as how and where to write the logic. The basic idea is resource thinking. Logic is packed in resource = Model. I will list how I can think of it and put a reference URL on it.

Validation is included in principle. Tight → can be loose, but loose → cannot be tight

The constraints should be tight.

For the same reason, Migration's foreign key constraints are basic.

The constraints are tight.

# frozen_string_literal: true

class CreateShopPhotos < ActiveRecord::Migration[5.2]
  def change
    create_table :shop_photos do |t|
      t.references :shop, index: true, foreign_key: true, null: false
      t.string :image_url, null: false
      t.timestamps
    end
  end
end
If you get stuck, leave it in Fat Model for the time being

If you write the logic in Model, it is easy to write unit test, so you can safely touch it later.

Search logic that uses where etc. is defined by scope. Not defined in instance method and class method.

If you define it with scope, you can also change from Instance from Class, but if you define the search logic with instance method or class method, you can only use either one.

Write only basic default index, show, create, update, delete in controller

Be as careful as possible to avoid mysterious actions when others see them.

Advantages of DHH-style routing and points to incorporate --KitchHike Tech Blog How DHH writes Rails controllers/| POSTD

In principle, use Serialzier for API response

Microservices Oriented Rails API Development Guide/building rails api on microservices --Speaker Deck The story of laying RESTful API rails on Rails apps and greatly increasing productivity \ | Wantedly Engineer Blog Laying RESTful API rails on Rails-Qiita

Pay attention to the argument of Sidekiq

As I wanted to increase the argument, I will add a hash type payload at the end for the time being.

Sidekiq Anti-Patterns: Introduction

I've never done it, but I wonder if I'll do it in the future

-I want to put in committee × OpenAPI and test the response with request spec.

Other

Ruby is flexible to write, so if you decide how to write it, I will refer to cookpad's style guide for the time being. Cookpad's style guide

At the end

Since I often developed by myself, I just digged on the web to improve my experience. This article is like a summary of the articles that have been influenced by it. If something is wrong with this, please comment. Rails in 2020!

List of reference articles

rubocop-rails Organize Rails and surrounding TimeZone settings (active_record.default_timezone trap) (https://qiita.com/joker1007/items/2c277cca5bd50e4cce5e) Rails Time Zone Summary I summarized the exception handling and error page display of Rails application Advantages of DHH-style routing and points to incorporate --KitchHike Tech Blog How DHH writes Rails controllers/| POSTD Microservices Oriented Rails API Development Guide/building rails api on microservices --Speaker Deck The story of laying RESTful API rails on Rails apps and greatly increasing productivity \ | Wantedly Engineer Blog Laying RESTful API rails on Rails-Qiita Cookpad's style guide Sidekiq Anti-Patterns: Introduction How to create a web application with Rails that can withstand operation How to defeat Fat Model serodriguez68/rails-antipatterns rspec style guide

Recommended Posts

Introducing the settings and reference articles that are first done in Rails new development
Install Rails in the development environment and create a new application
Options for rails new and settings to be done after rails new
[Rails6] The development that is done while manually compiling js using webpack was very smooth and impressed.
The problem that the contents of params are completely displayed in the [Rails] view
Errors and solutions and reference articles that occurred when putting Heroku in Vagrant
This and that of conditional branching of rails development
Fixed the phenomenon that vuetify built in Rails is not loaded when page transition is done
Filter the Java No. 2 Optional list that was useful in business and get the first value
[Rails] Why using find_by in scope and returning all records instead of nil if there are no records that match the conditions