The Clean Way to Handle Sendbird Webhook Using Ruby on Rails

Hi, as I said before in my first blog I want to share about design patterns for Ruby. So I will share substantial reasons for the existence of design patterns and how a design pattern solves your common problems in Ruby. And to make it clear and understandable, I will explain it using a good example: Sendbird Webhook. Before start, you can read the documentation here.

rails_sendbird

And for you who doesn’t know about design pattern, a design pattern is a general, typical solution to common problems in Software Engineering. So I think it’s a must for a developer to know at least one design pattern, especially Ruby developer. Why? Because Ruby is flexible, so we need something that can keep our codebases clean and understandable for every developer.

In this blog, I will explain my favorite design pattern, which is the Command Pattern. So are you ready? Let’s start then.

Like the others, Sendbird only needs one endpoint to handle all kinds of events. That part is very interesting because the command pattern can solve that problem. So firstly create a controller for it.

**app 
 |_controllers
    |_sendbird_controller.rb**

And create a new action on it: webhook. Don’t forget to add it to routes.rb.

class SendbirdController
 def webhook
  status: 200
 end
end

Since Sendbird doesn’t care about our process, just respond with status: 200 *immediately. And create a worker to handle the payload from Sendbird. Why using the worker? First, because Sendbird only sends the request 3 times until it receives *status: 200. And our workers can save the payload and retry the process as many as we want if we got a problem until the problem is gone. Second, because we need to respond immediately to avoid too many requests to our server. Third, hmm I think that’s it.

app 
 |_controllers
    |_sendbird_controller.rb
** |_workers
    |_sendbird
       |_webhook_worker.rb**

And put the worker on sendbird_controller.

class SendbirdController
 def webhook
  ::Sendbird::WebhookWorker.perform_later(params)
  status: 200
 end
end

Before we start coding the worker, let’s see Sendbird request params:

{
    'category': 'open_channel:create',
    'created_at': 1540866408000,
    'operators': [
        {
            'user_id': 'Jay',
            'nickname': 'Mighty',
            'profile_url': '[https://sendbird.com/main/img/profiles/profile_26_512px.png'](https://sendbird.com/main/img/profiles/profile_26_512px.png'),
            'metadata': {}
        }
    ],
    'channel': {
        'name': 'Jeff and friends',
        'channel_url': 'sendbird_open_channel_1_2681099203cd6b78414fe672927a43fcf3a30f09',
        'custom_type': '',
        'is_distinct': false,
        'is_public': false,
        'is_super': false,
        'is_ephemeral': false,
        'is_discoverable': false,
        'data': ''
    },
    'app_id': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}

The params above represent the command. And there is category *that represents the event of the command and can be a key for command pattern. We can see two parts on *category *value: *open_channel which is the resource and *create *which is the event of the resource. If we’re using the traditional way, the worker code will be like this:

module Sendbird
 class WebhookWorker
  def perform(params)
   if params['category'] == 'open_channel:create'
    # do something
   elsif params['category] == 'open_channel:update'
    # do something
   ...
   end
  end
 end
end

Or

module Sendbird
 class WebhookWorker
  def perform(params)
   case params['category']
   when 'open_channel:create'
    # do something
   when 'open_channel:update'
    # do something
   ...
   end
  end
 end
end

So what will happen next if we want to implement all kinds of events? Can you imagine that? LOL

So here the clean way to solve that problem:

module Sendbird
 class WebhookWorker
  attr_reader :params, :klass
  
  def self.perform(params)
   new(params).perform
  end

  def initialize(params)
   module_name, klass_name = params['category'].split(':')
   @params = params
   @klass = "::Sendbird::Webhook::#{module_name.camelize}::#{klass.camelize}".constantize
  end

  def perform
   klass.new(params).perform
  end
 end
end

To put the logic for each resource and event, we only need to create a new service. For example, open_channel:create. Create a new service here:

app 
 |_controllers
    |_sendbird_controller.rb
 **|_services
    |_sendbird
       |_webhook
          |_open_channel
             |_create.rb**
 |_workers
    |_sendbird
       |_webhook_worker.rb

With this code:

module Sendbird
 module Webhook
  module OpenChannel
   class Create
    attr_reader params
    
    def initialize(params)
     @params = params
    end

    def perform
     # do something when create open_channel event happens
    end
   end
  end
 end
end

If we want to handle a new event, simply create a new service. For example, now we want to handle *group_channel:update. *Just create a new service:

app 
 |_controllers
    |_sendbird_controller.rb
 |_services
    |_sendbird
       |_webhook
          **|_group_channel
             |_update.rb**
          |_open_channel
             |_create.rb
 |_workers
    |_sendbird
       |_webhook_worker.rb

With this code:

module Sendbird
 module Webhook
  module GroupChannel
   class Update
    attr_reader params
    
    def initialize(params)
     @params = params
    end

    def perform
     # do something when update group_channel event happens
    end
   end
  end
 end
end

Simple right? With this method, we can follow *rubocop *rules to avoid long class or method line length and make the file readable. But you will have many files, which is that’s okay for me.

I think that’s all. Thank you!

This post originally shared at Medium