Skip to content


Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time
Interact with remote REST services in an ActiveRecord-like manner.

Gem Version

Spyke basically rips off takes inspiration 😇 from Her, a gem which we sadly had to abandon as it gave us some performance problems and maintenance seemed to have gone stale.

We therefore made Spyke which adds a few fixes/features needed for our projects:

  • Fast handling of even large amounts of JSON
  • Proper support for scopes
  • Ability to define custom URIs for associations
  • ActiveRecord-like log output
  • Handling of API-side validations
  • Googlable name! :)


Add this line to your application's Gemfile:

gem 'spyke'
gem 'multi_json' # or whatever is needed to parse responses

Spyke uses Faraday to handle requests and expects it to parse the response body into a hash in the following format:

{ data: { id: 1, name: 'Bob' }, metadata: {}, errors: {} }

So, for example for an API that returns JSON like this:

{ "result": { "id": 1, "name": "Bob" }, "extra": {}, "errors": {} }

...the simplest possible configuration that could work is something like this:

# config/initializers/spyke.rb

class JSONParser < Faraday::Middleware
  def on_complete(env)
    json = MultiJson.load(env.body, symbolize_keys: true)
    env.body = {
      data: json[:result],
      metadata: json[:extra],
      errors: json[:errors]

Spyke::Base.connection = '') do |c|
  c.request   :multipart
  c.request   :json # if using Faraday 1.x, please add `faraday_middleware` to your dependencies first
  c.use       JSONParser
  c.adapter   Faraday.default_adapter


Adding a class and inheriting from Spyke::Base will allow you to interact with the remote service:

class User < Spyke::Base
  has_many :posts
  scope :active, -> { where(active: true) }

# => GET
# => GET

User.where(age: 3).active
# => GET

user = User.find(3)
# => GET

# => find embedded in returned data or GET

user.update(name: 'Alice')
# => PUT - { user: { name: 'Alice' } }


User.create(name: 'Bob')
# => POST - { user: { name: 'Bob' } }

Custom URIs

You can specify custom URIs on both the class and association level. Set uri to nil for associations you only want to use data embedded in the response and never call out to the API.

class User < Spyke::Base
  uri 'people(/:id)' # id optional, both /people and /people/4 are valid

  has_many :posts, uri: 'posts/for_user/:user_id' # user_id is required
  has_one :image, uri: nil # only use embedded data

class Post < Spyke::Base

user = User.find(3) # => GET
user.image # Will only use embedded data and never call out to api
user.posts # => GET
Post.find(4) # => GET

Custom requests

Custom request methods and the with scope methods allow you to perform requests for non-REST actions:

Post.with('posts/recent') # => GET
Post.with(:recent) # => GET
Post.with(:recent).where(status: 'draft') # => GET
Post.with(:recent).post # => POST

Custom requests from instance:

Post.find(3).put(:publish) # => PUT

Arbitrary requests (returns plain Result object):

Post.request(:post, 'posts/3/log', time: '12:00')
# => POST - { time: '12:00' }

Custom primary keys

Custom primary keys can be defined with self.primary_key = :custom_key:

class User < Spyke::Base
  self.primary_key = :user_id

  # When using custom URIs the :id parameter also has to be adjusted
  uri 'people(/:user_id)'

API-side validations

Spyke expects errors to be formatted in the same way as the ActiveModel::Errors details hash, ie:

{ title: [{ error: 'blank'}, { error: 'too_short', count: 10 }] }

If the API you're using returns errors in a different format you can remap it in Faraday to match the above. Doing this will allow you to show errors returned from the server in forms and f.ex using @post.errors.full_messages just like ActiveRecord.

Error handling and fallbacks

Should the API fail to connect or time out, a Spyke::ConnectionError will be raised. If you need to recover gracefully from connection problems, you can either rescue that exception or use the with_fallback feature:

# API is down
Article.all # => Spyke::ConnectionError
Article.with_fallback.all # => []

Article.find(1) # => Spyke::ConnectionError
Article.with_fallback.find(1) # => nil

article = Article.with_fallback( "Dummy")).find(1)
article.title # => "Dummy"


Spyke, like Rails, by default wraps sent attributes in a root element, but this can be disabled or customized:

class Article < Spyke::Base
  # Default
  include_root_in_json true # { article: { title: ...} }

  # Custom
  include_root_in_json :post # { post: { title: ...} }

  # Disabled
  include_root_in_json false # { title: ... }

Using multiple APIs

If you need to use different APIs, instead of configuring Spyke::Base you can configure each class individually:

class Post < Spyke::Base
  self.connection = '') do |faraday|
    # middleware

Log output

When used with Rails, Spyke will automatically output helpful ActiveRecord-like messages to the main log:

Started GET "/posts" for at 2014-12-01 14:31:20 +0000
Processing by PostsController#index as HTML
  Parameters: {}
  Spyke (40.3ms)  GET [200]
Completed 200 OK in 75ms (Views: 64.6ms | Spyke: 40.3ms | ActiveRecord: 0ms)

Other examples

For more examples of how Spyke can be used, check out fixtures.rb and the test suite.