Handling integrations in RoR
Nowadays almost all software projects have at least one integration. At Wolox, we have encountered many of those. For example, my last project required two parallel asynchronous integrations that gave the system commercial and financial information about customers who requested online loans. The process of building them can be tricky usually needing more information than is available. In order to deal with them, here are some useful things to take into account:
Responsibilities separation
It’s crucial to know the components you count with and the responsibilities you should delegate for each of them. For instance, controllers are the main entrance points to your system, as so, they should only have IO related responsibilities that separate the outside from the system’s internal classes.
Secondly, you have services. As their name indicates, they provide the different services you need for a certain model. This usually means they are the ones that interact with external APIs to be able to obtain the information needed for that object.
For example:
# app/models/book.rbclass Book < ApplicationRecord
end# app/controllers/books_controller.rb
class BooksController < ApplicationController
def create
book = Book.new(BooksService.fetch_info(create_params[:isbn])
render json: { book: book }, status: :created if book.save
end
end# app/services/books_service.rb
class BooksService
def fetch_info(isbn)
# Interact with external API that returns the book's info based on
isbn and adapted to the book model's attributes
end
end
Dealing with async integrations
Handling the integration’s requests can be tricky, especially when they are necessary for the application’s main flow. To simplify this matter, we developed Async Requests, a ruby gem that handles executing background tasks using sidekiq while giving the client an endpoint to verify the status.
Here’s a little example:
class MyController < ApplicationController
def myIntegrationEndpoint
job = AsyncRequest::Job.create_and_enqueue(
MyIntegrationWorker, { some: ‘args’ },
‘another arg’
)
render json: { token: job.token, url: async_request.job_url },
status: :accepted
end
endclass MyIntegrationWorker
def execute(options, string)
# Call services
[:ok, { message: ‘success’ }.to_json]
end
end
With that little code, you can enqueue your job while getting an URL to check your request’s current status until you can get the response.
Testing integrations
Tests involving integrations can be difficult to make. We usually build our controllers/services without having testing in mind, and when the testing moment comes, we sacrifice a lot of the test’s expressiveness in order to make them work without touching the code. A solution to this is the webmock gem. It allows you to stub requests providing predefined responses, even custom responses based on the request made.
For example:
stub_request(:any, ‘www.example.net').to_return { |request| {body: request.body} }
Responds with your requests body to any request going to www.example.com.
It’s really expressive while keeping your tests clean!
Another useful tool regarding tests is shared contexts. These are a great way of sharing initialization or other needed pieces of code that might be used in some, but not all tests.
You can define one by using:
# spec/support/shared_stuff.rbRSpec.shared_context “shared stuff”, :shared_context => :metadata do
before { @some_var = :some_value }
def shared_method
“it works”
end
let(:shared_let) { {‘arbitrary’ => ‘object’} }
subject do
‘this is the subject’
end
end
After that you can include them in your tests using: include_context “shared stuff”
Taking care of credentials
There are no doubt credentials can’t be stored in files pushed to Github, even with private repos there’s a risk of them being stolen. The most common solution is storing them in environment variables. For this, dotenv is a no-brainer, it allows you to load your env vars from a file on each server run while in the development environment. Just put a .env file in the project root and include all env vars you want like this::
# .env
VAR1=Value1
VAR2=Value2
Don’t forget to add your .env to the .gitignore!
#.gitignore.env
Despite environment variables being an easy way for credential handling, What if I told you there’s a newer and better one? Luckily for us, Rails 5.2 added encrypted credentials.
To use them, you only a need an encrypted config/credentials.yml.enc file (which despite being encrypted, it’s a regular .yml file containing your credentials) and a key to encrypt it located in /config/master.key (which you should never push to the repository).
After that, to edit the files you can use:
EDITOR=vim rails credentials:edit
Accessing them is really easy, just call Application.credentials.name_of_the_credential to get the values.
If you have a rails < 5.2 version in your app, you should edit your config/environments/*.rb
files and add config.require_master_key = true
To conclude, it’s important to know that when facing integrations, thinking of the future is the key. You never know how external services might change and that’s out of your control. To protect your code you should always prepare for the worst by separating all external interfaces from internal components.