Skip to content

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

Notifications You must be signed in to change notification settings

x1wins/tutorial-rails-rest-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

Contributors 3

  •  
  •  
  •  

Languages