Despite an existing Swagger-compatible API, most of Strava interactions written in Ruby don’t use any auto-generated code and prefer strava-api-v3 (update: this gem was yanked in 2024), a thin client that covers the majority of the Strava API. This is a much cleaner than any swagger-generated mess, but only comes with bare minimum extras. For example, the client can retrieve an activity, but does not have any code to convert a distance from meters to miles or calculate an athlete’s pace. It will also leave you having to refer to the Strava documentation on whether distance
is in meters, yards or feet.
That seems reasonable for a thin client, but I’ve already had to copy-paste a ton of code between publishing my runs to github pages and my Strava Slack bot. I considered adding that code to the strava-api-v3 client, but found its implementation gross enough to write a brand new one.
Introducing strava-ruby-client.
Unlike strava-api-v3 provides complete OAuth refresh token flow support, a richer first class interface to Strava models, natively supports pagination and implements more consistent error handling.
The rest of this post is about implementation details.
This is not my first API client rodeo, following the very popular slack-ruby-client and a newer iex-ruby-client, so you can be sure I integrated many of the lessons learned into this work. If you’re building a Ruby client for an API, I strongly encourage you to reuse this as a boilerplate.
Basics
I always start with a README.md, LICENSE.md, CONTRIBUTING.md, CHANGELOG.md and RELEASING.md. Future updates will include UPGRADING.md.
Version
This is very straightforward, but many API clients don’t even include a version, much less announce themselves to servers properly.
We are going to use this in the API’s user agent.
And make sure we have a test.
Configuration
API clients tend to want to be configurable globally or directly in a client instance. This is a pretty interesting pattern with the following configuration class.
Note that this pattern can be extended deeper, and the configuration can be further nested, as in iex-ruby-client#88.
The client itself.
What does this do? It allows global configuration.
And allows local configuration that overrides global configuration.
HTTP
For HTTP clients I prefer to use Faraday, which allows clients to compose modules in a single pipeline, including Faraday::FlatParamsEncoder
to convert input arguments into the HTTP GET query string, FaradayMiddleware::ParseJson
to parse JSON in response or Faraday::Response::RaiseError
to raise exceptions on HTTP errors. This also allows the caller to swap the HTTP engine in the future, often desired in complex applications that want fewer ways to HTTP.
I breakup the code into Web::Connection
and Web::Request
, easily gaining global configuration for the much needed proxy
or timeout
options.
These are mixed into the client class and reuse options from Config::ATTRIBUTES
.
Models
JSON API responses are parsed by Faraday into a Hash
, but I prefer first-class objects that can be extended. I like Hashie::Trash
, despite being demoniacally possessed.
This allows for first class, strongly typed instance properties and for future extensibility if the API adds fields or a stronger contract with Hashie::Extensions::IgnoreUndeclared
deleted.
You can also easily extend the class with additional methods.
API Calls
A basic API GET
call.
Something with parameters. I choose to use an extensible Hash
of options
, but YMMV.
A collection of objects.
Pagination
Clients should help with pagination. I typically roll out a cursor and wrap all collection API calls into it, enabling developers to automatically paginate through results by supplying a block. This can be seen in strava-ruby-client@40f2a0fd.
Famous Last Words
Writing clients is fun. If you are stuck with a crappy client with poor error handling or data modeling, roll your own based on strava-ruby-client in less than a day’s worth of work.