Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Delivering backend - Case study

Delivering backend - Case study

It's a short description of the project I've done recently. Focused on presenting stack, best practices, and patterns used. This our typical approach to Ruby backend at Netguru.


Michał Poczwardowski

March 21, 2018


  1. D C 21 /03/2018 - TRUG Michał Poczwardowski michal.poczwardowski@netguru.co dmp

    @ 3cityIT slack
  2. Agenda • Intro • Stack • Code Samples • Patterns

    • 3rd-parties • Security Audit
  3. None
  4. Questions • Client? Huge Business (from scratch - after PDS)

    / corpo • Problem Domain? Recruitment / Employer Branding • Team? Netguru + outside agency (responsible for UI and visuals) • Stack? Grape API + ActiveAdmin for Backend / Frontend@Ember
  5. From scratch (1st commit) rails new --database=postgresql --skip-test --skip-coffee --skip-action-cable

  6. README.md

  7. README.md - minimum! • Project stack • Project setup •

    Detailed info about used patterns and solutions • Known Issues • How to get into staging? • How to get into production? • Deployments explained • Default seeds
  8. Codebeat

  9. Stack / Gemfile (tip: keep them organized) #1 # rails

    gem "rails", "~> 5.1.3" # general gems gem "aasm" gem "activeadmin" gem "acts-as-taggable-on" gem "acts_as_list" gem "dry-monads" gem "jbuilder", "~> 2.5" gem "pg", "~> 0.18" gem "puma", "~> 3.7" gem "rails-i18n", "~> 5.0.0" gem "redis-namespace" gem "rollbar" gem "sidekiq"
  10. Stack / Gemfile #2 # authentication & authorization gem "devise"

    gem "devise_security_extension", git: "https://github.com/phatworx/devise_security_extension.git" # => rubygems.org devise_security_extension 0.10.0 version does not support rails 5.1 # => raising undefined method "before_filter" error gem "doorkeeper" gem "pundit" gem "rack-attack" gem "rack-cors"
  11. Stack / Gemfile #3 # grape + jsonapi gem "grape"

    gem "grape-jsonapi-resources" gem "grape-middleware-logger" gem "grape-swagger" gem "grape-swagger-rails" gem "hashie-forbidden_attributes" # to make grape params validation work
  12. Stack / Gemfile #4 # 3rd parties - APIs and

    processing gem "cloudinary" gem "griddler" gem "griddler-sendgrid" gem "httpi" # picked as a connection wizard because already needed by savon gem "nokogiri" gem "recaptcha", require: "recaptcha/rails" gem "savon", "~> 2"
  13. API - (almost) JSONAPI compliant (+ swagger)

  14. Endpoints Base class API::V1::Users::Base < Core namespace :users do mount

    ChangePassword mount Me mount Register route_param :id, type: Integer do before { @user = User.find(params[:id]) } mount Delete mount Show mount Update end end # app/controllers/api/v1/users/base.rb
  15. Example Endpoint - just authorize and service call class API::V1::Users::ChangePassword

    < Base desc "Change user password using current one" helpers Params helpers API::V1::Helpers::JSONAPIParams before { doorkeeper_authorize! } params do use :jsonapi_data_attributes, required: [{ name: "current-password", type: String, desc: "Current user password" }], use: [{ predefined: :_password_confirmable }] end patch "change-password" do authorize current_user, :change_password? assure_rightness ::UserServices::ChangePasswordUsingCurrent.call(current_user, jsonapi_attributes(params)) end end # app/controllers/api/v1/users/change_password.rb
  16. ApplicationService class UserServices::Register < ApplicationServiceInTransaction def call(attributes) Right(attributes) .bind(method(:ensure_agreements)) .bind

    { |attrs| ::GeneralServices::InjectModelIdsFromListInAttributes.call(attrs, :city) } .bind(method(:create_user)) end private def ensure_agreements(attributes) return Right(attributes) if required_agreements?(attributes) Left(I18n.t("user.registration.no_required_agreements")) end (...) end end # app/services/user_services/register.rb
  17. Models without logic and before/after actions class Message < ApplicationRecord

    belongs_to :author, class_name: "User", optional: true belongs_to :conversation has_many :activity_records, as: :trackable validates :content, presence: true end
  18. dry-monads http://dry-rb.org/gems/dry-monads/ Result The Result monad is useful to express

    a series of computations that might return an error object with additional information. The Result mixin has two type constructors: Success and Failure. The Success can be thought of as “everything went success” and the Failure is used when “something has gone wrong”. // http://dry-rb.org/gems/dry-monads/result/
  19. Why handle services using Either/Success monad • .succcess? and .failure?

    work the same for every service, • chainability, • universal error handling - the same interface for all services • ApplicationServiceInTransaction, • extract logic into shared pieces
  20. Integrations aka 3rd parties • KENEXA - WSDL/SOAP requests -

    recruitment cloud software, poorly documented, a lot of pain • Sendgrid Inbound Parse - handling incoming emails • Cloudinary - assets images/videos
  21. What could be done better? • JSONAPI - jsonapi-resources creates

    links that are not correct
  22. Security Audit

  23. https://securityheaders.io

  24. Secure headers https://github.com/twitter/secureheaders SecureHeaders::Configuration.default do |config| # CSP stands for

    Content Security Policy config.csp = { base_uri: %w('self'), block_all_mixed_content: true, child_src: %w('self'), connect_src: [], default_src: %w('self'), font_src: %w('self' data:), form_action: %w('self'), frame_ancestors: [], img_src: %w('self'), manifest_src: %w('self'), media_src: [], object_src: %w('self'), plugin_types: [], report_uri: [], sandbox: false, script_src: %w('self'), style_src: %w('self' 'unsafe-inline'), upgrade_insecure_requests: false, worker_src: %w('self'), } config.referrer_policy = %w(no-referrer-when-downgrade) end # config/initializers/secure_headers.rb
  25. Questions / Thanks!