3 Ways to Configure Your Ruby API Wrappers

When you use Ruby to wrap an API, you have to have a way to configure it. Maybe the wrapper needs a username and secret key, or maybe just a host.

There are a few different ways to handle this. So which one should you choose?

The easy, global way

You might want your service to act like it’s always around. No matter where you are in your app, you’d have it ready to use. Otherwise, you’ll spend three lines of configuring it for every line of using it!

You could make the configuration global, using constants or class attributes:

config/initializers/product_api.rb
ProductApi.root = "https://staging-host.example.com/"
ProductApi.user = "justin"
ProductApi.secret = "mysecret123"
app/controllers/products_controller.rb
def show
  @product = ProductApi.find(params[:id])
end

Lots of gems use this pattern. It’s pretty easy to write, and really easy to use. But it has some big problems:

  • You can only have one ProductApi.

    If you want to use the Product API as two different users, or hit different servers from a single app, you’re out of luck.

  • ProductApi has global data that’s easy to accidentally change.

    If a thread or a part of your app changed ProductApi.user, everything else using ProductApi would break. And those are painful bugs to track down.

So, class variables have some problems. What if you configured instances of your Product API class, instead?

What would it look like with #initialize?

If you used instances, you’d create and configure your API wrapper when you need it:

app/controllers/products_controller.rb
def show
  product_api = ProductApi.new(
    root: "https://staging-host.example.com/",
    user: "justin",
    secret: "mysecret123")
  @product = product_api.find(params[:id])
end

Now, you can pass different details to your API whenever you use it. No other methods or threads are using your instance, so you don’t have to worry about it changing without you knowing it.

This seems better. But it’s still not as easy as it should be. Because you have to configure your API every time you use it.

Most of the time you don’t care how the API is set up, you just want to use it with sane options. But when you’re working with instances, every part of your app that uses the API has to know how to configure it.

But there’s a way to get the convenience of global access, using good defaults, while still being able to change it if you need to.

And this pattern shows up all the time in an interesting place: OS X and iOS development.

How do you get good defaults and flexibility?

What if you could configure each instance of your API wrapper, but you also had a global “default” instance when you just didn’t care?

You’ll see this “defaultSomething” or “sharedWhatever” pattern all over the iOS and Mac OS SDKs:

[[NSURLSession sharedSession] downloadTaskWithURL:@"http://www.google.com"];

[[NSFileManager defaultManager] removeItemAtPath:...];

And you can still ask for instances of these classes if you need more than what the default gives you:

NSURLSession *session = [NSURLSession sessionWithConfiguration:...];

NSFileManager fileManager = [[NSFileManager alloc] init];

You could build something like that in Ruby, with a default_api class method:

app/controllers/products_controller.rb
def show
  @product = ProductApi.default_product_api.find(params[:id])
end

...

def show_special
  special_product_api = ProductApi.new(
    root: "https://special-product-host.example.com/"
    user: "justin"
    secret: "mysecret123")
  @special_product = special_product_api.find(params[:id])
end

And the implementation might look something like this:

class ProductApi
  def initialize(root:, user:, secret:)
    @root, @user, @secret = root, user, secret
  end

  def self.default_api
    @default_api ||= new(
      root: ENV['PRODUCT_API_ROOT'],
      user: ENV['PRODUCT_API_USER'],
      secret: ENV['PRODUCT_API_SECRET'])
  end

  def find(product_id)
    ...
  end
end

Here, I used environment variables in default_api, but you could also use config files. And you could switch the ||= to use thread- or request-local storage instead.

But this is a decent start.


Most gems I’ve seen, like the Twitter gem, will have you configure and create each API object when you need them. This is an OK solution (though I usually see people assigning these to globals anyway).

But if you go one step further, and also use a pre-configured default object, you’ll have a much more comfortable time.

Pushing through tutorials, and still not learning anything?

Have you slogged through the same guide three times and still don't know how to build a real app?

In this free 7-day Rails course, you'll learn specific steps to start your own Rails apps — without giving up, and without being overwhelmed.

You'll also discover the fastest way to learn new Rails features with your 32-page sample of Practicing Rails: Learn Rails Without Being Overwhelmed.

Sign up below to get started:

Powered by ConvertKit

Did you like this article? You should read these:

Comments