Skip to content

Tutorial category, post, comment, user, rest api with rails, rspec, JWT

Notifications You must be signed in to change notification settings

Arlantir/tutorial-rails-rest-api

 
 

Repository files navigation

Build Status Heroku Inline docs HitCount contributions welcome Known Vulnerabilities

Deploy

Tutorial rails rest api

We always need rest api server with json response for client.
I try developing best practice Restful Api with rails or another framework. This tutorial use Ruby on Rails Api-only Applications.
I hope no one more suffer from many developing methods such a Unit testing with Rspec, Token base Authenticate and Authorized, Api Documentation, Storage config for upload, Log collecting .. etc

Index

Feature

Prerequisites

Default storage config is Cloudinary. but i did not push master.key
you have to generate master.key and add your storage config

  1. Changing master.key

    1. Delete old master.key and credentials.yml.enc https://www.chrisblunt.com/rails-on-docker-rails-encrypted-secrets-with-docker/
          $ rm config/master.key config/credentials.yml.enc
    2. create new master.key and credentials.yml.enc
      • docker-compose
            $ docker-compose run --rm -e EDITOR=vim web bin/rails credentials:edit
      • Non docker-compose
            $ EDITOR=vim web bin/rails credentials:edit   
      • result
            Adding config/master.key to store the encryption key: c7713a458177982b0d951fd50649b674
            
            Save this in a password manager your team can access.
            
            If you lose the key, no one, including you, can access anything encrypted with it.
            
                  create  config/master.key
            
            File encrypted and saved.
  2. Clodinary config

    open EDITOR=vim rails credentials:edit or docker-compose run --rm -e EDITOR=vim web bin/rails credentials:edit

    • you can join free plan Cloudinary and get PROJECT_NAME, API_KEY, API_SECRET
        cloudinary:
          cloud_name: PROJECT_NAME
          api_key: API_KEY
          api_secret: API_SECRET

How to Install and Run Tutorial rails rest api Project in your local

  • You can choice for run with docker-compose or Non docker-compose. You shold better use docker-compose
    • download from github
          git clone https://github.com/x1wins/tutorial-rails-rest-api.git
    • docker-compose

      1. Build and Run with Background demon
            docker-compose up --build -d
      2. Database Setup
            docker-compose run web bundle exec rake db:test:load && \
            docker-compose run web bundle exec rake db:migrate && \
            docker-compose run web bundle exec rake db:seed --trace
      3. Another docker-compose Command for rails and rake
        • How do I update Gemfile.lock on my Docker host? https://stackoverflow.com/a/37927979/1399891
              docker-compose run --no-deps web bundle
        • How to remove images after building https://forums.docker.com/t/how-to-remove-none-images-after-building/7050
              docker rmi $(docker images -f “dangling=true” -q)
        • Database Reset
              docker-compose run web bundle exec rake db:reset --trace
        • Log
          • Development enviroment
                tail -f log/development.log # if you wanna show sql log
          • Production enviroment
                tail -f log/production.log 
        • Testing with rspec

              docker-compose run --no-deps web bundle exec rspec --format documentation
              docker-compose run --no-deps web bundle exec rspec --format documentation spec/requests/api/v1/upload_spec.rb
              docker-compose run --no-deps web bundle exec rspec --format documentation spec/requests/api/v1/posts_spec.rb
              docker-compose run --no-deps web bundle exec rspec --format documentation spec/controllers/api/v1/posts_controller_spec.rb
        • Rswag for documentation http://localhost:3000/api-docs/index.html
              docker-compose run --no-deps web bundle exec rake rswag
        • rails console
              docker-compose exec web bin/rails c
        • routes
              docker-compose run --no-deps web bundle exec rake routes
    • Non docker-compose

      1. bundle
            bundle install
      2. postgresql run
            rake docker:pg:init
            rake docker:pg:run
      3. migrate
            rake db:migrate RAILS_ENV=test
            rake db:migrate
            rake db:seed
      4. redis run
           docker run --rm --name my-redis-container -p 6379:6379 -d redis redis-server --appendonly yes
           redis-cli -h localhost -p 7001
      5. server run
            rails s
      6. Another Command for rails and rake
        • Database Reset
              rake db:reset --trace
        • Log
          • Development enviroment
                tail -f log/development.log # if you wanna show sql log
          • Production enviroment
                tail -f log/production.log 
        • Testing
              bundle exec rspec --format documentation
              bundle exec rspec --format documentation spec/requests/api/v1/upload_spec.rb
              bundle exec rspec --format documentation spec/requests/api/v1/posts_spec.rb
              bundle exec rspec --format documentation spec/controllers/api/v1/posts_controller_spec.rb
        • Rswag for documentation http://localhost:3000/api-docs/index.html
             rake rswag 
        • rails console
              rails c
        • routes
              rake routes

Deploy on Production server

i did deploy to heroku. let's break it down with swagger UI
https://tutorial-rails-rest-api.herokuapp.com/api-docs/index.html
there will be auto added Heroku Redis free plan add-on, Heroku Postgresql free plan add-on, Cloudinary free plan add-on Deploy

TODO

Tutorial Index - Rails rest api for post

Storage config for Upoload

you can change active storage config to such a like cloud storage S3 or GCS in storage.yml

if you use heroku and you upload file on local path of Ephemeral Disk. Uploaded file will be gone in a few minutes because heroku hard drive is Ephemeral Disk

Authentication

    class ApplicationController < ActionController::API
      def authorize_request
        header = request.headers['Authorization']
        header = header.split(' ').last if header
        begin
          @decoded = JsonWebToken.decode(header)
          @current_user = User.find(@decoded[:user_id])
          is_banned @current_user
        rescue ActiveRecord::RecordNotFound => e
          render json: { errors: e.message }, status: :unauthorized
        rescue JWT::DecodeError => e
          render json: { errors: e.message }, status: :unauthorized
        end
      end
    end
    
    # How to Use
    class PostsController < ApplicationController
        before_action :authorize_request
    end

Authorize

    class ApplicationController < ActionController::API
      def is_owner user_id
        unless user_id == @current_user.id
          render json: nil, status: :forbidden
          return
        end
      end
    
      def is_owner_object data
        if data.nil? or data.user_id.nil?
          return render status: :not_found
        else
          is_owner data.user_id
        end
      end
    end
    
    # How to Use
    class PostsController < ApplicationController
        before_action only: [:update, :destroy, :destroy_attached] do
          is_owner_object @post ##your object
        end
    end        

Build Json with active_model_serializers Gem

  1. Gemfile
      gem 'active_model_serializers'
  2. Generate Serializer
    1. Generate Serializer to Exist Model user, post
        rails g serializer user name:string username:string email:string
        rails g serializer post body:string user:references published:boolean
    2. Generate Serializer New Model comment
        rails g scaffold comment body:string post:references user:references published:boolean
        rails g serializer comment body:string user:references published:boolean
  3. Add Model Attribute
      # app/serializers/post_serializer.rb
      class PostSerializer < ActiveModel::Serializer
        attributes :id, :body, :user, :comments
        has_one :user
        has_many :comments
      end
      # app/serializers/comment_serializer.rb
      class CommentSerializer < ActiveModel::Serializer
        attributes :id, :body, :user
        has_one :user
      end
      # app/serializers/user_serializer.rb
      class UserSerializer < ActiveModel::Serializer
        attributes :id, :name, :username, :email
      end
  4. For Nested model serializer
      # config/initializers/active_model_serializer.rb
      ActiveModelSerializers.config.default_includes = '**'
  5. Pagination with serializer

/app/helpers/category_helper.rb

# app/helpers/category_helper.rb
module CategoryHelper
  def fetch_categories pagaination_param
    page = pagaination_param[:category_page]
    per = pagaination_param[:category_per]
    key = "categories"+pagaination_param.to_s
    categories =  $redis.get(key)
    if categories.nil?
      @categories = Category.published.by_date.page(page).per(per)
      categories = Pagination.build_json(@categories, pagaination_param).to_json
      $redis.set(key, categories)
      $redis.expire(key, 1.hour.to_i)
    end
    categories
  end
  def clear_cache_categories
    keys = $redis.keys "*categories*"
    keys.each {|key| $redis.del key}
  end
end

class CategoriesController < ApplicationController
      include CategoryHelper
      //... your code

      # GET /categories
      def index
        page = params[:page].presence || 1
        per = params[:per].presence || Pagination.per
        pagaination_param = {
            category_page: page,
            category_per: per,
            post_page: @post_page,
            post_per: @post_per
        }
        @categories = fetch_categories pagaination_param
        render json: @categories
      end
class Category < ApplicationRecord
  include CategoryHelper
  belongs_to :user
  has_many :posts
  scope :published, -> { where(published: true) }
  scope :by_date, -> { order('id DESC') }
  validates :title, presence: true
  validates :body, presence: true
  after_save :clear_cache_categories
end
class CategorySerializer < ActiveModel::Serializer
  attributes :id, :title, :body, :posts_pagination
  has_one :user
  has_many :posts
  def posts
    post_page = (instance_options.dig(:pagaination_param, :post_page).presence || 1).to_i
    post_per = (instance_options.dig(:pagaination_param, :post_per).presence || 0).to_i
    object.posts.published.by_date.page(post_page).per(post_per)
  end
  def posts_pagination
    post_per = (instance_options.dig(:pagaination_param, :post_per).presence || Pagination.per).to_i
    Pagination.build_json(posts)[:posts_pagination] if post_per > 0
  end
end
# /lib/pagination.rb
class Pagination
  def self.information array
    {
        current_page: array.current_page,
        next_page: array.next_page,
        prev_page: array.prev_page,
        total_pages: array.total_pages,
        total_count: array.total_count
    }
  end
  def self.build_json array, pagaination_param = {}
    ob_name = array.name.downcase.pluralize.to_sym
    json = Hash.new
    json[ob_name] = ActiveModelSerializers::SerializableResource.new(array.to_a, pagaination_param: pagaination_param)
    pagination_name = "#{ob_name}_pagination".to_sym
    json[pagination_name] = self.information array
    json
  end
end

Nested Model

  1. Comment Controller
      class CommentsController < ApplicationController
        before_action :authorize_request
        before_action :set_comment, only: [:show, :update, :destroy]
        before_action only: [:edit, :update, :destroy] do
          is_owner_object @comment ##your object
        end
        //...your code
        # Only allow a trusted parameter "white list" through.
        def comment_params
          params.require(:comment).permit(:body, :post_id).merge(user_id: @current_user.id)
        end
      end
  2. Model
      # app/models/post.rb
      class Post < ApplicationRecord
       belongs_to :user
       has_many :comments
      end
      # app/models/comment.rb
      class Comment < ApplicationRecord
        belongs_to :post
        belongs_to :user
      end

add published

  1. alter column
       $ rails generate migration ChangePublishedDefaultToComments published:boolean
        class ChangePublishedDefaultToComments < ActiveRecord::Migration[6.0]
          def change
            change_column :comments, :published, :boolean, default: true
          end
        end
  2. Add published = true condition for has_many In Model Serializer
        class PostSerializer < ActiveModel::Serializer
          attributes :id, :body
          has_one :user
          has_many :comments
          def comments
            object.comments.where(published: true).order('id DESC')
          end
        end

Category

  1. generate
        rails g scaffold category title:string body:string user:references published:boolean
  2. add referer
        rails g migration AddCategoryToPosts category:references
  3. migration
         # db/seed.rb
         user = User.create!({username: 'hello', email: 'sample@changwoo.net', password: 'hhhhhhhhh', password_confirmation: 'hhhhhhhhh'})
         category = Category.create!({title: 'all', body: 'you can talk everything', user_id: user.id})
         posts = Post.where(category_id: nil).or(Post.where(published: nil))
         posts.each do |post|
           post.category_id = category.id
           post.published = true
           post.save
           p post
         end
         p category
        rake db:seed

Post

  1. add title column
    rails g migration AddTitleToPosts title:string

model alter

  1. remove column
    rails g migration RemoveColumnFromTables column:type
  1. add column
    rails g migration AddColumnFromTables column:type
  1. add unique to name
    rails g migration AddUniqueNameToUsers

sample - add unique to user.name in generate file

  add_index :table_name, :column_name, unique: true

Testing

Facker gem

https://rubyinrails.com/2018/11/10/rails-building-json-api-resopnses-with-jbuilder/

```ruby
    gem 'faker', '~> 1.9.1', group: [:development, :test]
```

CURL

  1. Join User

        curl -d '{"user": {"name":"ChangWoo", "username":"CW", "email":"x1wins@changwoo.org", "password":"hello1234", "password_confirmation":"hello1234"}}' -H "Content-Type: application/json" -X POST -i http://localhost:3000/users
        curl -d '{"user": {"name":"hihi", "username":"helloworld", "email":"hello@changwoo.org", "password":"hello1234", "password_confirmation":"hello1234"}}' -H "Content-Type: application/json" -X POST -i http://localhost:3000/users
  2. Login

        curl -d '{"email":"x1wins@changwoo.org", "password":"hello1234"}' -H "Content-Type: application/json" -X POST http://localhost:3000/auth/login | jq
        curl -d '{"email":"hello@changwoo.org", "password":"hello1234"}' -H "Content-Type: application/json" -X POST http://localhost:3000/auth/login | jq
  3. Create Post

        curl  -X POST -i http://localhost:3000/posts -d '{"post": {"body":"sample body text sample"}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY"
        curl  -X POST -i http://localhost:3000/posts -d '{"post": {"body":"hihihi ahaha"}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY"
        curl  -X POST -i http://localhost:3000/posts -d '{"post": {"body":"Average Speed   Time    Time     Time  Current"}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE1Nzc0OTMwMjl9.s9WqkyM84LQGZUtpmfmZzWN8rsVUp4_yfKfxEN_t4AQ"

    file upload - create

        curl -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODE1MjgwNjd9.YKkk0B-T0_AROBTVaQ7f_OE2hnFGp1HcR2wbEDa9EtA" \
        -F "post[body]=string123" \
        -F "post[category_id]=1" \
        -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \
        -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \
        -X POST http://localhost:3000/api/v1/posts

    file upload - delete

        curl -X DELETE "http://localhost:3000/api/v1/posts/731/attached/93" \
        -H  "accept: application/json" \
        -H  "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODE1NDY0Njl9.XjaDElIlvmWDyAWMiGtjZByax-IuG1HBn3i8-Rjl1EU"

    file upload - update

        curl -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODE1MjgwNjd9.YKkk0B-T0_AROBTVaQ7f_OE2hnFGp1HcR2wbEDa9EtA" \
        -F "post[body]=aasadsadasdasstring123" \
        -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \
        -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \
        -X PUT http://localhost:3000/api/v1/posts/728
  4. Index Post

        curl -X GET http://localhost:3000/posts -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY" | jq
  5. Create Comment

        curl -X POST -i http://localhost:3000/comments -d '{"comment": {"body":"sample body for comment", "post_id": 2}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY"
  6. loop curl

        for i in {1..100}; do bundle exec rspec; done
        for i in {1..10000}; do curl -X GET "http://localhost:3000/categories?page=1" -H "accept: application/json" -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODA1NzY5NDZ9.vjoQpeOKdX83JwAwkPBi6p-dWjc1MPGVUQsSG9QSWhg"; done
        ab -n 10000 -c 100 -H "accept: application/json" -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODA1NzY5NDZ9.vjoQpeOKdX83JwAwkPBi6p-dWjc1MPGVUQsSG9QSWhg" -v 2 http://localhost:3000/categories?page=1

    if you want stop for loop

        pkill rspec

    login sample curl

        curl -w "\n" -X POST "http://localhost:3000/auth/login" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"email\": \"hello@changwoo.org\", \"password\": \"hello1234\"}" >> curl.log

rswag for API Documentation

  1. testing bundle exec rspec spec/requests/users_spec.rb --format documentation

  2. Generate documentation

    this command rake rswag will generate swag documentation. then you can connect to http://localhost:3000/api-docs/index.html

    swagger_screencapture

Codegen

We developed server side code and We shoud need Client code. you can use Swagger-Codegen https://github.com/swagger-api/swagger-codegen#swagger-code-generator

https://github.com/swagger-api/swagger-codegen/wiki/FAQ#how-can-i-generate-an-android-sdk

    brew install swagger-codegen
    mkdir -p /var/tmp/java/okhttp-gson/
    swagger-codegen generate -i http://localhost:3000/api-docs/v1/swagger.yaml \
      -l java --library=okhttp-gson \
      -D hideGenerationTimestamp=true \
      -o /var/tmp/java/okhttp-gson/  

Caused by: android.os.NetworkOnMainThreadException https://www.toptal.com/android/android-threading-all-you-need-to-know

sample https://i.stack.imgur.com/ytin1.png https://gist.github.com/just-kip/1376527af60c74b07bef7bd7f136ff56

        AsyncTask<Post, Void, Post> asyncTask = new AsyncTask<Post, Void, Post>() {
            @Override
            protected Post doInBackground(Post... params) {
                try {
                    ApiClient defaultClient = Configuration.getDefaultApiClient();
                    String authorization = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODIwOTg3NzF9.JGPR2oOOeGcjSocU4Ohvw1bg49ZjTQ9tQ3FtxmqmPDM"; // String | JWT token for Authorization
                    ApiKeyAuth Bearer = (ApiKeyAuth) defaultClient.getAuthentication("Bearer");
                    Bearer.setApiKey(authorization);
                    PostApi apiInstance = new PostApi();
                    String id = "1"; // String | id
                    Integer commentPage = 1; // Integer | Page number for Comment
                    Integer commentPer = 10; // Integer | Per page number For Comment
                    Post result;
                    try {
                        result = apiInstance.apiV1PostsIdGet(id, authorization, commentPage, commentPer);
//                        System.out.println(result);
                    } catch (ApiException e) {
                        System.err.println("Exception when calling PostApi#apiV1PostsIdGet");
                        e.printStackTrace();
                        result = new Post();
                    }
                    return result;
                } catch (Exception e) {
                    e.printStackTrace();
                    return new Post();
                }
            }

            @Override
            protected void onPostExecute(Post post) {
                super.onPostExecute(post);
                if (post != null) {
                    mEmailView.setText(post.getBody());
                    System.out.print(post);
                }
            }
        };

        asyncTask.execute();

https://www.sitepoint.com/consuming-web-apis-in-android-with-okhttp/

No Network Security Config specified, using platform default Use 10.0.2.2 to access your actual machine. https://stackoverflow.com/questions/5528850/how-do-you-connect-localhost-in-the-android-emulator // private String basePath = "https://tutorial-rails-rest-api.herokuapp.com"; // private String basePath = "http://localhost:3000"; private String basePath = "http://10.0.2.2:3000";

swagger-annotations Unable to pre-dex https://stackoverflow.com/questions/43997544/execution-failed-for-task-java-lang-runtimeexceptionunable-to-pre-dex

dexOptions {
    javaMaxHeapSize "2g" // set it to 4g will bring unable to start JavaVirtualMachine
    preDexLibraries = false
  }

Log For ELK stack (Elastic Search, Logstash, Kibana)

elk.yml config

enable value is false or true
exmaple : enable: false
elk.yml

# config/elk.yml

default: &default
  enable: false
  protocal: udp
  host: localhost
  port: 5000

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

lograge.rb with custom config

https://github.com/roidrage/lograge
https://ericlondon.com/2017/01/26/integrate-rails-logs-with-elasticsearch-logstash-kibana-in-docker-compose.html
lograge.rb

    Rails.application.configure do
      enable = Rails.configuration.elk['enable']
      protocal = Rails.configuration.elk['protocal']
      host = Rails.configuration.elk['host']
      port = Rails.configuration.elk['port']
    
      if enable
        config.autoflush_log = true
        config.lograge.base_controller_class = 'ActionController::API'
        config.lograge.enabled = true
        config.lograge.formatter = Lograge::Formatters::Logstash.new
        config.lograge.logger = LogStashLogger.new(type: protocal, host: host, port: port, sync: true)
        config.lograge.custom_options = lambda do |event|
          exceptions = %w(controller action format id)
          {
              type: :rails,
              environment: Rails.env,
              remote_ip: event.payload[:ip],
              email: event.payload[:email],
              user_id: event.payload[:user_id],
              request: {
                  headers: event.payload[:headers],
                  params: event.payload[:params].except(*exceptions)
              }
          }
        end
      end
    
    end

application.rb https://guides.rubyonrails.org/v4.2/configuring.html#custom-configuration

override append_info_to_payload for lograge, append_info_to_payload method put parameter to payload[]

        class ApplicationController < ActionController::API
          #...leave out the details
        
          def append_info_to_payload(payload)
            super
            payload[:ip] = remote_ip(request)
            if @current_user.present?
              begin
                user = User.find(@current_user.id)
                payload[:email] = user.email
                payload[:user_id] = user.id
              rescue ActiveRecord::RecordNotFound => e
                payload[:email] = ''
                payload[:user_id] = ''
              end
            end
          end
        
          def remote_ip(request)
            request.headers['HTTP_X_REAL_IP'] || request.remote_ip
          end
        end

Redis

server run

docker run --rm --name my-redis-container -p 7001:6379 -d redis redis-server --appendonly yes
docker run --rm --name my-redis-container -p 7001:6379 -d redis 
redis-cli -h localhost -p 7001

add gem

gem 'redis'
gem 'redis-namespace'
gem 'redis-rails'
gem 'redis-rack-cache'

config

# config/initializers/redis.rb

$redis = Redis::Namespace.new("tutorial_post", :redis => Redis.new(:host => '127.0.0.1', :port => 7001))

how to added cache

  # GET /categories
  def index
    page = params[:page].presence || 1
    per = params[:per].presence || Pagination.per
    pagaination_param = {
        category_page: page,
        category_per: per,
        post_page: @post_page,
        post_per: @post_per
    }
    @categories = fetch_categories pagaination_param
    render json: @categories
  end
    class Category < ApplicationRecord
      include CategoryHelper
      ...your code
      after_save :clear_cache_categories
    end
    # app/helpers/category_helper.rb
    module CategoryHelper
      def fetch_categories pagaination_param
        page = pagaination_param[:category_page]
        per = pagaination_param[:category_per]
        key = "categories"+pagaination_param.to_s
        categories =  $redis.get(key)
        if categories.nil?
          @categories = Category.published.by_date.page(page).per(per)
          categories = Pagination.build_json(@categories, pagaination_param).to_json
          $redis.set(key, categories)
          $redis.expire(key, 1.hour.to_i)
        end
        categories
      end
      def clear_cache_categories
        keys = $redis.keys "*categories*"
        keys.each {|key| $redis.del key}
      end
    end

Active Storage

Setup

https://cameronbothner.com/activestorage-beyond-rails-views/ https://edgeguides.rubyonrails.org/active_storage_overview.html
https://edgeguides.rubyonrails.org/active_storage_overview.html#has-many-attached

    rails active_storage:install
    rake db:migrate

storage.yml you wiil make dir /storage with mkdir /storage

    test:
      service: Disk
      root: /storage/test
    
    local:
      service: Disk
      root: /storage

routes.rb

    Rails.application.routes.draw do
      namespace :api do
        namespace :v1 do
          # your code will be here ...
          # DELETE /posts/attached/:attached_id
          delete '/posts/:id/attached/:attached_id', to: 'posts#destroy_attached'
        end
      end
    end

posts_controller.rb
@post.files.attach(params[:post][:files]) if params.dig(:post, :files).present?is null check and add files

      # posts_controller
      # POST /posts
      def create
        @post = Post.new(post_params)
        @post.files.attach(params[:post][:files]) if params.dig(:post, :files).present?

        set_category @post.category_id

        if @post.save
          render json: @post, status: :created, location: api_v1_post_url(@post)
        else
          render json: @post.errors, status: :unprocessable_entity
        end
      end

      # PATCH/PUT /posts/1
      def update
        @post.files.attach(params[:post][:files]) if params.dig(:post, :files).present?
        if @post.update(post_params)
          render json: @post
        else
          render json: @post.errors, status: :unprocessable_entity
        end
      end

      # DELETE /posts/:id/attached/:id
      def destroy_attached
        attachment = ActiveStorage::Attachment.find(params[:attached_id])
        attachment.purge # or use purge_later
      end

post.rb

    class Post < ApplicationRecord
      # your code will be here ...
      has_many_attached :files
    end
    class PostSerializer < ActiveModel::Serializer
      include Rails.application.routes.url_helpers
      attributes :id, :body, :files, :comments_pagination
      def files
        return unless object.files.attachments
        file_urls = object.files.map do |file|
          {
              id: file.id,
              url: rails_blob_url(file)
          }
        end
        file_urls
      end
      # your code will be here ...     
    end

apache bench mark

https://gist.github.com/kelvinn/6a1c51b8976acf25bd78 bash ab -c 10 -n 10000 \ -T application/json \ -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1OTA0MzExOTN9.GtlH3xbINMNSKAU00np5njGtDWEcXXOHZ2zbjKsgr24" \ http://localhost:3000/api/v1/posts?category_id=1&page=1

About

Tutorial category, post, comment, user, rest api with rails, rspec, JWT

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 99.4%
  • Other 0.6%