1
\$\begingroup\$

I am trying to create an API wrapper for recharge (A Shopify subscription service), I am using the HTTParty gem

module RechargeAPI require 'httparty' BASE_URI = 'https://api.rechargeapps.com' API_TOKEN = 'my_token' class Client include HTTParty base_uri BASE_URI headers 'X-Recharge-Access-Token': API_TOKEN end class Customer < Client def self.search(params) response = get('/customers', query: params) self.from_json(response.body) end def self.find(params) self.search(params).first end def self.all response = get('/customers') self.from_json(response.body) end def self.from_json(customers_json) customers = JSON.parse(customers_json).dig('customers') customers.map do|customer| OpenStruct.new(customer) end end end end 

RechargeAPI::Customer.find(shopify_customer_id: 5363543224286) # returns <OpenStruct accepts_marketing=nil, analytics_data={"utm_params"=>[]}, billing_address1=....

It works fine, However i feel i am not using the best practices for writing an api wrapper. Ideally i would set my api token with something like RechargeAPI.api_token = 'token' rather than it being hardcoded or in an ENV file. But i dont know how then i would use headers 'X-Recharge-Access-Token': API_TOKEN

Also ideally RechargeAPI::Customer.find(shopify_customer_id: 5363543224863) would return a RechargeAPI::Customer object rather than an OpenStruct. I would love to be able to inherit from Struct but obviously i cannot as I am inheriting from my RechargeAPI::Client class.

Could anybody advise on how i could go about doing this, or any way to improve this code. Thankyou!

\$\endgroup\$
1

2 Answers 2

1
\$\begingroup\$

I mostly agree with Sidney. Depending how big the API is I would not split 1 (A class responsible for your HTTP requests to Shopify in general) and 2 (A class that calls your HTTP class to make the requests; it calls the right URLs and creates the domain objects).

Here is an example

module RechargeAPI require 'httparty' BASE_URI = 'https://api.rechargeapps.com' API_TOKEN = 'my_token' class Client include HTTParty base_uri BASE_URI headers 'X-Recharge-Access-Token': API_TOKEN def self.search_customers(params) JSON.parse(get('/customers', query: params)) end def self.customers JSON.parse(get('/customers')) end end end 

A good example how to implement an API wrapper is e.g. https://github.com/octokit/octokit.rb or https://github.com/Shopify/buildkit.

Then you can use your API wrapper to build your PORO / model classes like this

class Customer < Struct.new(:id, :name) cattr_accessor :client RechargeAPI::Client.new end def self.all client.customers.map do |params| new(params[:id], params[:name]) end end def self.search(params) client.search(params).map do |params| new(params[:id], params[:name]) end end def self.find(params) search(params).first end end 

Please note that I use a class attribute accessor (cattr_accessor) to instantiate the client on the Customer class which is implement in ActiveSupport / Rails. This has the advantage that you don't need to inherit from your API class anymore and you can now use dependency injection to replace the client. For instance, in testing you can now use a dummy client instead of doing real HTTP calls.

class DummyClient self.customers { id: 1, name: 'name' } end end def test_customer_all_returns_customers Customer.client = DummyClient.new assert_equal 1, Customer.all.count customer = Customer.first assert_equal 1, customer.id assert_equal 'name', customer.name end 
\$\endgroup\$
    1
    \$\begingroup\$

    I think you should have three classes:

    1. A class responsible for your HTTP requests to Shopify in general.
    2. A class that calls your HTTP class to make the requests; it calls the right URLs and creates the domain objects.
    3. The Customer class, which should be more independent, should not depend on the above classes; in DDD terms, it's a domain object.

    In this approach, you will be using composition over inheritance.

    \$\endgroup\$
    4
    • 1
      \$\begingroup\$I like that. A Customer is not really an HTTP Client but rather a resource and should just encapsulate the business/domain logic (and maybe persistence if needed).\$\endgroup\$CommentedOct 2, 2021 at 20:29
    • \$\begingroup\$What would i do then if i needed to access a nested resource, e.g. customer_instance.products, Lets say in this example i get products by going to the endpoint /products?customer_id=foo\$\endgroup\$CommentedOct 12, 2021 at 17:50
    • \$\begingroup\$I would do it all with pure Ruby. The product would be a new model called Product, then Customer#products points to an Array of Products. You can have a factory that, from the JSON, creates the Customer and its list of products.\$\endgroup\$
      – m26a
      CommentedOct 12, 2021 at 21:18
    • \$\begingroup\$There maybe be some gem that helps you achieve or simplify what I've said. I didn't mention any gem because I don't know any, and they may not be necessary depending on your use case. It depends on how much you are used to using the gem and the complexity.\$\endgroup\$
      – m26a
      CommentedOct 12, 2021 at 21:21

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.