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
Tweet

More Decks by Michał Poczwardowski

Other Decks in Programming

Transcript

  1. D
    C
    21 /03/2018 - TRUG
    Michał Poczwardowski
    [email protected]
    dmp @ 3cityIT slack

    View Slide

  2. Agenda
    ● Intro
    ● Stack
    ● Code Samples
    ● Patterns
    ● 3rd-parties
    ● Security Audit

    View Slide

  3. View Slide

  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

    View Slide

  5. From scratch (1st commit)
    rails new
    --database=postgresql
    --skip-test
    --skip-coffee
    --skip-action-cable
    --skip-yarn

    View Slide

  6. README.md

    View Slide

  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

    View Slide

  8. Codebeat

    View Slide

  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"

    View Slide

  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"

    View Slide

  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

    View Slide

  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"

    View Slide

  13. API - (almost) JSONAPI compliant (+ swagger)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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/

    View Slide

  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

    View Slide

  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

    View Slide

  21. What could be done better?
    ● JSONAPI - jsonapi-resources creates links that are not correct

    View Slide

  22. Security Audit

    View Slide

  23. https://securityheaders.io

    View Slide

  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

    View Slide

  25. Questions / Thanks!

    View Slide