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
- Demo
- Feature
- Prerequisites
- How to Install and Run Tutorial rails rest api Project in your local
- Deploy on Production server
- TODO
- Tutorial Index - Rails rest api for post
- supported Unit Testing with Rspec
- supported Document with Rswag
gem 'rswag-api'gem 'rswag-ui'gem 'rswag-specs'https://github.com/rswag/rswag - supported Docker-compose
- supported Heroku
- supported ELK for logs with
gem 'lograge' - used Ruby:2.6.0 with Dockerfile
- used Rails 6
- used Active Storage for Upload file with Cloudiry free plan (Required config key)
- used
gem 'active_model_serializers'for json response - used
gem 'jwt-rails', '~> 0.0.1'for token based Authentication, Authorize - used
gem 'kaminari'for pagination - used postgresql with Active record
- used redis for cache
- used https://app.snyk.io/ for security
Default storage config is Cloudinary. but i did not push master.key
you have to generate master.key and add your storage config
-
- 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
- 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.
- docker-compose
- Delete old master.key and credentials.yml.enc https://www.chrisblunt.com/rails-on-docker-rails-encrypted-secrets-with-docker/
-
open
EDITOR=vim rails credentials:editordocker-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
- You can choice for run with
docker-composeorNon docker-compose. You shold better usedocker-compose- download from github
git clone https://github.com/x1wins/tutorial-rails-rest-api.git
-
- Build and Run with Background demon
docker-compose up --build -d
- Update
Gemfile.lockdocker-compose run --no-deps web bundle docker-compose up --build -d
- Update
- 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
- Another docker-compose Command for
railsandrake- 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
- Development enviroment
-
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.htmldocker-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
- How do I update Gemfile.lock on my Docker host? https://stackoverflow.com/a/37927979/1399891
- Build and Run with Background demon
-
- bundle
bundle install
- postgresql run
rake docker:pg:init rake docker:pg:run
- migrate
rake db:migrate RAILS_ENV=test rake db:migrate rake db:seed
- redis run
docker run --rm --name my-redis-container -p 6379:6379 -d redis redis-server --appendonly yes redis-cli -h localhost -p 7001
- server run
rails s
- Another Command for
railsandrake- 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
- Development enviroment
- 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.htmlrake rswag
- rails console
rails c
- routes
rake routes
- Database Reset
- bundle
- download from github
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 addedHeroku Redis free plan add-on,Heroku Postgresql free plan add-on,Cloudinary free plan add-on
- Heroku
- install CLI https://devcenter.heroku.com/articles/heroku-cli#download-and-install
brew tap heroku/brew && brew install heroku - Login heroku
https://dashboard.heroku.com/apps/YOUR_PORJECT_NAME/deploy/heroku-git
heroku login heroku git:clone -a YOUR_PORJECT_NAME cd YOUR_PORJECT_NAME - migration
heroku rake db:migrate --app YOUR_PORJECT_NAME heroku rake db:seed --app YOUR_PORJECT_NAME
- Another cmd
- master.key
heroku config:set RAILS_MASTER_KEY=asdf1234 --app YOUR_PORJECT_NAME
- restart
heroku restart --app YOUR_PORJECT_NAME
- log
heroku logs --tail --app YOUR_PORJECT_NAME
- console with heroku
heroku run rails console --app YOUR_PORJECT_NAME
- master.key
- install CLI https://devcenter.heroku.com/articles/heroku-cli#download-and-install
- Docker compose in your server
- ssh
ssh -i ~/your.pem ec2-user@ec2-your-code.compute.amazonaws.com - install git, docker with yum on aws ec2 instance https://www.changwoo.org/x1wins@changwoo.net/2019-09-19/aws-setting-with-docker-git-cfac5c7d1b
sudo yum update -y sudo yum install docker sudo service docker start sudo usermod -a -G docker ec2-user sudo yum install git
- git clone
git clone https://github.com/x1wins/tutorial-rails-rest-api.git cd tutorial-rails-rest-api/ - change master.key
- docker-compose
- ssh
- Generate porject
rails new [Project Name] --api -T -d postgresql - Database setting Gem https://github.com/x1wins/docker-postgres-rails
- User scaffold
- User scaffold and JWT for user authenticate Gem https://github.com/x1wins/jwt-rails
- User role http://railscasts.com/episodes/189-embedded-association?view=asciicast https://github.com/ryanb/cancan/wiki/Role-Based-Authorization
- avatar file upload
- generate uninque username https://alexcastano.com/generate-unique-usernames-for-ruby-on-rails/
- Category scaffold
- fix post.category serialize
- Post scaffold
- add title column
- Comment scaffold
- add depth
- file upload
- Model Serializer https://itnext.io/a-quickstart-guide-to-using-serializer-with-your-ruby-on-rails-api-d5052dea52c5
- Rspec https://relishapp.com/rspec/rspec-rails/docs/gettingstarted
- Swager https://github.com/rswag/rswag
- Add published condition of association https://www.rubydoc.info/gems/active_model_serializers/0.9.4
- Search in posts
- Pagination https://github.com/kaminari/kaminari
- categories#index
- posts#index
- posts#index Comments
- posts#show Comments
- Add json of pagination
- Parent Model 404 check in Nested Model
- Parent Category in Post#index 404 check
- Post rspec
- Parent Post, Category in Comment#index 404 check
- Comment rspec
- Parent Category in Post#index 404 check
- N+1
- log
- Versioning http://railscasts.com/episodes/350-rest-api-versioning?view=asciicast
- File upload to Local path with active storage
- create or add attached file
- delete
- docker-compose
- staging
- production
you can change active storage config to such a like cloud storage
S3 or GCSin storage.ymlif 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
- Local
- Add
~/storagepath for saving uploaded filemkdir ~/storage - Update
config.active_storage.service = :localin development.rb, production.rb - Added local config in storage.yml
- Add
-
- https://cloudinary.com/documentation/rails_activestorage
- https://github.com/0sc/activestorage-cloudinary-service
- Added api key
- Add gemfile
gem 'cloudinary' gem 'activestorage-cloudinary-service'
- open
config/storage.ymlcloudinary: service: Cloudinary cloud_name: <%= Rails.application.credentials.dig(:cloudinary, :cloud_name) %> api_key: <%= Rails.application.credentials.dig(:cloudinary, :api_key) %> api_secret: <%= Rails.application.credentials.dig(:cloudinary, :api_secret) %>
- Changing master.key
- Clodinary config
- Add gemfile
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 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 - Gemfile
gem 'active_model_serializers' - Generate Serializer
- 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
- 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
- Generate Serializer to Exist Model user, post
- 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
- For Nested model serializer
# config/initializers/active_model_serializer.rb ActiveModelSerializers.config.default_includes = '**'
- 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 endclass 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 endclass 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- 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
- 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
- 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
- Add
published = truecondition for has_many In Model Serializerclass PostSerializer < ActiveModel::Serializer attributes :id, :body has_one :user has_many :comments def comments object.comments.where(published: true).order('id DESC') end end
- generate
rails g scaffold category title:string body:string user:references published:boolean
- add referer
rails g migration AddCategoryToPosts category:references
- 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
- add title column
rails g migration AddTitleToPosts title:string- remove column
rails g migration RemoveColumnFromTables column:type- add column
rails g migration AddColumnFromTables column:type- add unique to name
rails g migration AddUniqueNameToUserssample - add unique to user.name in generate file
add_index :table_name, :column_name, unique: truehttps://rubyinrails.com/2018/11/10/rails-building-json-api-resopnses-with-jbuilder/
```ruby gem 'faker', '~> 1.9.1', group: [:development, :test] ``` -
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
-
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
-
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
-
Index Post
curl -X GET http://localhost:3000/posts -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY" | jq
-
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"
-
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
-
testing
bundle exec rspec spec/requests/users_spec.rb --format documentation -
Generate documentation
this command
rake rswagwill generate swag documentation. then you can connect to http://localhost:3000/api-docs/index.html
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 }enable value is
falseortrue
exmaple :enable: false
elk.yml
# config/elk.yml default: &default enable: false protocal: udp host: localhost port: 5000 development: <<: *default test: <<: *default production: <<: *defaulthttps://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 endapplication.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 enddocker 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 7001gem 'redis' gem 'redis-namespace' gem 'redis-rails' gem 'redis-rack-cache'# config/initializers/redis.rb $redis = Redis::Namespace.new("tutorial_post", :redis => Redis.new(:host => '127.0.0.1', :port => 7001)) # 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 endhttps://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:migratestorage.yml you wiil make dir /storage with
mkdir /storage
test: service: Disk root: /storage/test local: service: Disk root: /storageroutes.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 endposts_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 endpost.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 ... endhttps://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
