0.0
The project is in a healthy, maintained state
A simple way to serve Twirp RPC services in a Rails app. Minimial configuration and familiar Rails conventions.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

>= 7.0.0
>= 1.8.0
 Project Readme

Gem Version CI Ruby Style Guide

Twirp on Rails (Twirp::Rails)

Motivation

Serving Twirp RPC Services should be as easy and familiar as Rails controllers. We add a few helpful abstractions, but don't hide Twirp, Protobufs, or make it seem too magical.

Out of the box, the twirp gem lets you add Services, but it feels clunky coming from Rails REST-ful APIs. We make it simple to build full-featured APIs. Hook in authorization, use before_action and more.

Extracted from a real, production application with many thousands of users.

Installation

Install the gem using gem install twirp-on-rails or simply add it to your Gemfile:

gem "twirp-on-rails"

Usage

Add to your routes.rb:

mount Twirp::Rails::Engine, at: "/twirp"

Generate your _pb.rb and _twirp.rb files

Generate files how Twirp-Ruby recommends.

Example:

protoc --ruby_out=./lib --twirp_ruby_out=./lib  haberdasher.proto

We (currently) don't run protoc for you and have no opinions where you put the generated files.

Ok, one small opinion: we default to looking in lib/, but you can change that.

Configuration

Twirp::Rails will automatically load any *_twirp.rb files in your app's lib/ directory (and subdirectories). To modify the location, add this to an initializer:

Rails.application.config.load_paths = ["lib", "app/twirp"]

Features

Easy Routing

Add one line to your config/routes.rb and routes are built automatically from your Twirp Services:

mount Twirp::Rails::Engine, at: "/twirp"

/twirp/twirp.example.haberdasher.HaberdasherService/MakeHat

These are routed to Handlers in app/handlers/ based on expected naming conventions.

For example if you have this service defined:

package twirp.example.haberdasher;

service HaberdasherService {
   rpc MakeHat(Size) returns (Hat);
 }

it will expect to find app/handlers/haberdasher_service_handler.rb with a make_hat method.

class HaberdasherServiceHandler < Twirp::Rails::Handler
  def make_hat

  end
end

Each handler method should return the appropriate Protobuf, or a Twirp::Error.

Packages and Namespacing

Handlers can live in directories that reflect the service's package. For example, haberdasher.proto defines:

package twirp.example.haberdasher;

You can use the full path, or because many projects have only one namespace, we also let you skip the namespace for simplicity:

We look for the handler in either location:

app/handlers/twirp/example/haberdasher/haberdasher_service_handler.rb defines Twirp::Example::Haberdasher::HaberdasherServiceHandler

or

app/handlers/haberdasher_service_handler.rb defines HaberdasherServiceHandler

TODO: Give more examples of handlers

Familiar Callbacks

Use before_action, around_action, and other callbacks you're used to, as we build on AbstractController::Callbacks.

rescue_from

Use rescue_from just like you would in a controller:

class HaberdasherServiceHandler < Twirp::Rails::Handler
  rescue_from "ArgumentError" do |error|
    Twirp::Error.invalid_argument(error.message)
  end

  rescue_from "Pundit::NotAuthorizedError", :not_authorized

  ...
end

DRY Service Hooks

Apply Service Hooks one time across multiple services.

For example, we can add hooks in an initializer:

# Make IP address accessible to the handlers
Rails.application.config.twirp.service_hooks[:before] = lambda do |rack_env, env|
  env[:ip] = rack_env["REMOTE_ADDR"]
end

# Send exceptions to Honeybadger
Rails.application.config.twirp.service_hooks[:exception_raised] = ->(exception, _env) { Honeybadger.notify(exception) }

Middleware

As an Engine, we avoid all the standard Rails middleware. That's nice for simplicity, but sometimes you want to add your own middleware. You can do that by specifying it in an initializer:

Rails.application.config.twirp.middleware = [Rack::Deflater]

Bonus Features

Outside the Twirp spec, we have some (optional) extra magic. They might be useful to you, but you can easily ignore them too.

Basic Caching with ETags/If-None-Match Headers

Like Rails GET actions, Twirp::Rails handlers add ETag headers based on the response's content.

If you have RPCs that can be cached, you can have your Twirp clients send an If-None-Match Header. Twirp::Rails will return a 304 Not Modified HTTP status and not re-send the body if the ETag matches.

Enable by adding this to an initializer:

Rails.application.config.twirp.middleware = [
  Twirp::Rails::Rack::ConditionalPost,
  Rack::ETag
]

TODO

  • More docs!
  • More tests!
  • installer generator to add ApplicationHandler
    • Maybe a generator for individual handlers that adds that if needed?
  • Auto reload.
  • Make service hooks more configurable? Apply to one service instead of all?
  • Loosen Rails version requirement? Probably works, but haven't tested.

Prior Art

We evaluated all these projects and found them to be bad fits for us, for one reason or another. We're grateful to all for their work, and hope they continue and flourish. Some notes from our initial evaluation:

nikushi/twirp-rails

  • Nice routing abstraction
  • Minimal Handler abstraction
  • Untouched for 4 years

cheddar-me/rails-twirp

  • Too much setup.
  • Nice controllers, but expects you to use their pbbuilder which I find unnecessary.

severgroup-tt/twirp_rails-1

  • Some nice things
  • No Handler abstractions
  • Archived and not touched for 3 years

dudo/rails_respond_to_pb

  • Allows routing to existing controllers
  • I dislike the respond_to stuff. That shouldn't be something you think about. We have a better way to do that in other recent apps anyway.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/danielmorrison/twirp-rails.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

License

The gem is available as open source under the terms of the MIT License.