{"id":19052506,"url":"https://github.com/collectiveidea/twirp-rails","last_synced_at":"2025-04-24T02:41:36.479Z","repository":{"id":65368146,"uuid":"581632259","full_name":"collectiveidea/twirp-rails","owner":"collectiveidea","description":"Make serving a Twirp RPC Services as easy and familiar as Rails controllers.","archived":false,"fork":false,"pushed_at":"2025-01-28T23:41:22.000Z","size":172,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-18T10:23:44.759Z","etag":null,"topics":["protobuf","rails","rpc","ruby","twirp"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/collectiveidea.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-12-23T19:25:03.000Z","updated_at":"2025-01-28T23:13:24.000Z","dependencies_parsed_at":"2023-02-09T12:20:10.946Z","dependency_job_id":"1521883a-e5fe-4aa5-8105-2c785e5ffdd9","html_url":"https://github.com/collectiveidea/twirp-rails","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Ftwirp-rails","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Ftwirp-rails/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Ftwirp-rails/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Ftwirp-rails/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/collectiveidea","download_url":"https://codeload.github.com/collectiveidea/twirp-rails/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250550419,"owners_count":21449036,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["protobuf","rails","rpc","ruby","twirp"],"created_at":"2024-11-08T23:26:48.092Z","updated_at":"2025-04-24T02:41:36.466Z","avatar_url":"https://github.com/collectiveidea.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Gem Version](https://img.shields.io/gem/v/twirp-on-rails.svg)](https://rubygems.org/gems/twirp-on-rails)\n[![CI](https://github.com/collectiveidea/twirp-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/collectiveidea/twirp-rails/actions/workflows/ci.yml)\n[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)\n\n# Twirp on Rails (Twirp::Rails)\n\n## Motivation\n\nServing [Twirp](https://twitchtv.github.io/twirp/) RPC Services should be as easy and familiar as Rails controllers. We add a few helpful abstractions, but don't hide [Twirp](https://twitchtv.github.io/twirp/), [Protobufs](https://protobuf.dev), or make it seem too magical.\n\nOut of the box, the [`twirp` gem](http://github.com/github/twirp-ruby) lets you add [Services](https://github.com/github/twirp-ruby/wiki/Service-Handlers), 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.\n\nExtracted from a real, production application with many thousands of users.\n\n## Installation\n\nInstall the gem using `gem install twirp-on-rails` or simply add it to your `Gemfile`:\n\n```\ngem \"twirp-on-rails\"\n```\n\n## Usage\n\nAdd to your `routes.rb`:\n\n```ruby\nmount Twirp::Rails::Engine, at: \"/twirp\"\n```\n\n### Generate your `_pb.rb` and `_twirp.rb` files\n\nGenerate files [how Twirp-Ruby recommends](https://github.com/arthurnn/twirp-ruby/wiki/Code-Generation). \n\nExample: \n\n```bash\nprotoc --ruby_out=./lib --twirp_ruby_out=./lib  haberdasher.proto\n```\n\nWe (currently) don't run `protoc` for you and have no opinions where you put the generated files. \n\nOk, one small opinion: we default to looking in `lib/`, but you can change that.\n\n### Configuration\n\nTwirp::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: \n\n```ruby \nRails.application.config.load_paths = [\"lib\", \"app/twirp\"]\n```\n\n## Features\n\n### Easy Routing\n\nAdd one line to your `config/routes.rb` and routes are built automatically from your Twirp Services:\n\n```ruby\nmount Twirp::Rails::Engine, at: \"/twirp\"\n```\n\n`/twirp/twirp.example.haberdasher.HaberdasherService/MakeHat`\n\nThese are routed to Handlers in `app/handlers/` based on expected naming conventions.\n\nFor example if you have this service defined: \n\n```protobuf\npackage twirp.example.haberdasher;\n\nservice HaberdasherService {\n   rpc MakeHat(Size) returns (Hat);\n }\n```\n\nit will expect to find `app/handlers/haberdasher_service_handler.rb` with a `make_hat` method. \n\n```ruby\nclass HaberdasherServiceHandler \u003c Twirp::Rails::Handler\n  def make_hat\n\n  end\nend\n```\n\nEach handler method should return the appropriate Protobuf, or a `Twirp::Error`.\n\n#### Packages and Namespacing\n\nHandlers can live in directories that reflect the service's package. For example, `haberdasher.proto` defines:\n\n```protobuf\npackage twirp.example.haberdasher;\n```\n\nYou can use the full path, or because many projects have only one namespace, we also let you skip the namespace for simplicity:\n\nWe look for the handler in either location:\n\n`app/handlers/twirp/example/haberdasher/haberdasher_service_handler.rb` defines `Twirp::Example::Haberdasher::HaberdasherServiceHandler`\n\nor\n\n`app/handlers/haberdasher_service_handler.rb` defines `HaberdasherServiceHandler`\n\n\nTODO: Give more examples of handlers\n\n### Familiar Callbacks\n\nUse `before_action`, `around_action`, and other callbacks you're used to, as we build on [AbstractController::Callbacks](https://api.rubyonrails.org/classes/AbstractController/Callbacks.html).\n\n### rescue_from\n\nUse `rescue_from` just like you would in a controller: \n\n```ruby\nclass HaberdasherServiceHandler \u003c Twirp::Rails::Handler\n  rescue_from \"ArgumentError\" do |error|\n    Twirp::Error.invalid_argument(error.message)\n  end\n\n  rescue_from \"Pundit::NotAuthorizedError\", :not_authorized\n\n  ...\nend\n```\n\n### DRY Service Hooks\n\nApply [Service Hooks](https://github.com/twitchtv/twirp-ruby/wiki/Service-Hooks) one time across multiple services.\n\nFor example, we can add hooks in an initializer: \n\n```ruby\n# Make IP address accessible to the handlers\nRails.application.config.twirp.service_hooks[:before] = lambda do |rack_env, env|\n  env[:ip] = rack_env[\"REMOTE_ADDR\"]\nend\n\n# Send exceptions to Honeybadger\nRails.application.config.twirp.service_hooks[:exception_raised] = -\u003e(exception, _env) { Honeybadger.notify(exception) }\n```\n\n### Middleware\n\nAs 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:\n\n```ruby\nRails.application.config.twirp.middleware = [Rack::Deflater]\n```\n\n### Logging\n\nOur built-in logging outputs the result of each request. \n\nYou could replace our logger if you want different output: \n\n```ruby\nRails.application.config.twirp.logger = Rack::CommonLogger\n```\n\nAdditionally, you can log the full Twirp response object to help with debugging: \n\n```ruby\nRails.application.config.twirp.verbose_logging = true\n```\n\n## Bonus Features\n\nOutside the [Twirp spec](https://twitchtv.github.io/twirp/docs/spec_v7.html), we have some (optional) extra magic. They might be useful to you, but you can easily ignore them too.\n\n### Basic Caching with ETags/If-None-Match Headers\n\nLike Rails GET actions, Twirp::Rails handlers add [`ETag` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) based on the response's content.\n\nIf you have RPCs that can be cached, you can have your Twirp clients send an [`If-None-Match` Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match). Twirp::Rails will return a `304 Not Modified` HTTP status and not re-send the body if the ETag matches.\n\nEnable by adding this to an initializer:\n\n```ruby\nRails.application.config.twirp.middleware = [\n  Twirp::Rails::Rack::ConditionalPost,\n  Rack::ETag\n]\n```\n\nNote: The Handler will still be run, but you won't need to send back the response. Make sure your RPC is idempotent! Future versions hope to make it easier to short-circuit expensive parts of the handler. \n\n## TODO\n\n* More docs!\n* More tests!\n* installer generator to add `ApplicationHandler`\n    * Maybe a generator for individual handlers that adds that if needed?\n* Auto reload.\n* Make service hooks more configurable? Apply to one service instead of all?\n* Loosen Rails version requirement? Probably works, but haven't tested. \n\n## Prior Art\n\nWe 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:\n\n[nikushi/twirp-rails](https://github.com/nikushi/twirp-rails)\n\n* Nice routing abstraction\n* Minimal Handler abstraction\n* Untouched for 4 years\n\n[cheddar-me/rails-twirp](https://github.com/cheddar-me/rails-twirp)\n\n* Too much setup.\n* Nice controllers, but expects you to use their [pbbuilder](https://github.com/cheddar-me/pbbuilder) which I find unnecessary.\n\n[severgroup-tt/twirp_rails-1](https://github.com/severgroup-tt/twirp_rails-1)\n\n* Some nice things\n* No Handler abstractions\n* Archived and not touched for 3 years\n\n[dudo/rails_respond_to_pb](https://github.com/dudo/rails_respond_to_pb)\n\n* Allows routing to existing controllers\n* 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.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/danielmorrison/twirp-rails.\n\n## Development\n\nAfter 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.\n\nTo 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](https://rubygems.org).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcollectiveidea%2Ftwirp-rails","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcollectiveidea%2Ftwirp-rails","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcollectiveidea%2Ftwirp-rails/lists"}