{"id":13878855,"url":"https://github.com/vitoravelino/modular_routes","last_synced_at":"2025-11-11T18:40:10.778Z","repository":{"id":41976088,"uuid":"371524795","full_name":"vitoravelino/modular_routes","owner":"vitoravelino","description":"Dedicated controllers for each of your Rails route actions.","archived":false,"fork":false,"pushed_at":"2022-04-21T02:04:35.000Z","size":110,"stargazers_count":46,"open_issues_count":5,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-28T22:40:24.064Z","etag":null,"topics":["controller","gem","rails","routes","ruby","ruby-on-rails","rubygem"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vitoravelino.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-05-27T23:06:45.000Z","updated_at":"2024-01-25T15:47:02.000Z","dependencies_parsed_at":"2022-08-12T01:10:43.894Z","dependency_job_id":null,"html_url":"https://github.com/vitoravelino/modular_routes","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/vitoravelino/modular_routes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vitoravelino%2Fmodular_routes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vitoravelino%2Fmodular_routes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vitoravelino%2Fmodular_routes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vitoravelino%2Fmodular_routes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vitoravelino","download_url":"https://codeload.github.com/vitoravelino/modular_routes/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vitoravelino%2Fmodular_routes/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":276164322,"owners_count":25596021,"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","status":"online","status_checked_at":"2025-09-20T02:00:10.207Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["controller","gem","rails","routes","ruby","ruby-on-rails","rubygem"],"created_at":"2024-08-06T08:02:02.141Z","updated_at":"2025-09-20T21:55:12.629Z","avatar_url":"https://github.com/vitoravelino.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"[gem]: https://rubygems.org/gems/modular_routes\n[ci]: https://github.com/vitoravelino/modular_routes/actions/workflows/ci.yml\n[coverage]: https://github.com/vitoravelino/modular_routes/actions/workflows/coverage.yml\n[codeclimate]: https://codeclimate.com/github/vitoravelino/modular_routes\n\n# Modular Routes\n\n_Dedicated controllers for each of your Rails route actions._\n\n[![gem version](https://img.shields.io/gem/v/modular_routes?color=blue)][gem]\n[![CI](https://github.com/vitoravelino/modular_routes/actions/workflows/ci.yml/badge.svg)][ci]\n[![Coverage](https://github.com/vitoravelino/modular_routes/actions/workflows/coverage.yml/badge.svg)][coverage]\n[![Maintainability](https://img.shields.io/codeclimate/maintainability/vitoravelino/modular_routes)][codeclimate]\n\nIf you've ever used [Hanami routes](https://guides.hanamirb.org/v1.3/routing/restful-resources/) or already use dedicated controllers for each route action, this gem might be useful.\n\n**Disclaimer:** There's no better/worse nor right/wrong approach, it's up to you to decide how you prefer to organize the controllers and routes of your application.\n\nDocs: [Unreleased](https://github.com/vitoravelino/modular_routes/blob/main/README.md), [v0.3.0](https://github.com/vitoravelino/modular_routes/blob/v0.3.0/README.md), [v0.2.0](https://github.com/vitoravelino/modular_routes/blob/v0.2.0/README.md), [v0.1.1](https://github.com/vitoravelino/modular_routes/blob/v0.1.1/README.md)\n\n## Motivation\n\nLet's imagine that you have to design a full RESTful resource named `articles` with some custom routes like the table below\n\n| HTTP Verb | Path                  |\n| --------- | --------------------- |\n| GET       | /articles             |\n| GET       | /articles/new         |\n| POST      | /articles             |\n| GET       | /articles/:id         |\n| GET       | /articles/:id/edit    |\n| PATCH/PUT | /articles/:id         |\n| DELETE    | /articles/:id         |\n| GET       | /articles/stats       |\n| POST      | /articles/:id/archive |\n\n**How would you organize the controllers and routes of this application?**\n\nThe most common approach is to have all the actions (RESTful and customs) in the same controller.\n\n```ruby\n# routes.rb\n\nresources :articles do\n  get  :stats,   on: :collection\n  post :archive, on: :member\nend\n\n# articles_controller.rb\n\nclass ArticlesController\n  def index\n    # ...\n  end\n\n  def create\n    # ...\n  end\n\n  # other actions...\n\n  def stats\n    # ...\n  end\n\n  def archive\n    # ...\n  end\nend\n```\n\nThe reason I don't like this approach is that you can end up with a lot of code that are not related to each other in the same file. You can still have it all organized but I believe that it could be better.\n\n[DHH](http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/) prefers to keep the RESTful actions (index, new, edit, show, create, update, destroy) inside the same controller and the custom ones in dedicated controllers but represented as RESTful actions.\n\nOne way of representing that would be\n\n```ruby\n# routes.rb\n\nresources :articles do\n  get  :stats,   on: :collection, to: 'articles/stats#show'\n  post :archive, on: :member,     to: 'articles/archive#create'\nend\n\n# articles_controller.rb\n\nclass ArticlesController\n  def index\n    # ...\n  end\n\n  def create\n    # ...\n  end\n\n  # other actions...\nend\n\n# articles/archive_controller.rb\n\nclass Articles::ArchiveController\n  def create\n  end\nend\n\n# articles/stats_controller.rb\n\nclass Articles::StatsController\n  def show\n  end\nend\n```\n\nThis approach is better than the previous one because it restricts the main controller file to contain only the RESTful actions. Additional routes would require you to create a dedicated controller to handle that individually.\n\nAnother approach (and what I personally prefer) is to have one controller per route. What it was done for `archive` and `stats` routes would also be applied to all the RESTful routes.\n\nThe files would be organized inside `articles/` folder that would act as a namespace\n\n```\napp/\n└── controllers/\n    └── articles/\n        ├── archive_controller.rb\n        ├── create_controller.rb\n        ├── destroy_controller.rb\n        ├── edit_controller.rb\n        ├── index_controller.rb\n        ├── new_controller.rb\n        ├── show_controller.rb\n        ├── stats_controller.rb\n        └── update_controller.rb\n```\n\nAnd the controllers would have one single action named `call` like\n\n```ruby\n# articles/index_controller.rb\n\nclass Articles::IndexController\n  def call\n  end\nend\n\n# articles/archive_controller.rb\n\nclass Articles::ArchiveController\n  def call\n  end\nend\n```\n\nHere are two ways of representing what was explained above:\n\n```ruby\nscope module: :articles, path: '/articles' do\n  get    '/',        to: 'index#call', as: 'articles'\n  post   '/',        to: 'create#call'\n\n  get    'new',      to: 'new#call',  as: 'new_article'\n  get    ':id/edit', to: 'edit#call', as: 'edit_article'\n  get    ':id',      to: 'show#call', as: 'article'\n  patch  ':id',      to: 'update#call'\n  put    ':id',      to: 'update#call'\n  delete ':id',      to: 'destroy#call'\n\n  post 'stats',       to: 'stats#call',   as: 'stats_articles'\n  post ':id/archive', to: 'archive#call', as: 'archive_article'\nend\n```\n\nor\n\n```ruby\nresources :articles, module: :articles, only: [] do\n  collection do\n    get  :index,  to: 'index#call'\n    post :create, to: 'create#call'\n    post :stats,  to: 'stats#call'\n  end\n\n  new do\n    get :new, to: 'new#call'\n  end\n\n  member do\n    get    :edit,    to: 'edit#call'\n    get    :show,    to: 'show#call'\n    patch  :update,  to: 'update#call'\n    put    :update,  to: 'update#call'\n    delete :destroy, to: 'destroy#call'\n    post   :archive, to: 'archive#call'\n  end\nend\n```\n\nThis is the best approach in my opinion because your controller will contain only code related to that specific route action. It will also be easier to test and maintain the code.\n\nIf you've decided to go with the last approach, unless you organize your routes in [separated files](https://guides.rubyonrails.org/routing.html#breaking-up-very-large-route-file-into-multiple-small-ones), your `config/routes.rb` might get really messy as your application grows due to verbosity.\n\nSo, what if we had a simpler way of doing all of that? Let's take a look at how modular routes can help us.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"modular_routes\"\n```\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself as:\n\n    $ gem install modular_routes\n\n## Usage\n\n`modular_routes` uses Rails route helpers behind the scenes. So you can pretty much use everything except for a few [limitations](#limitations) that will be detailed later.\n\nFor the same example used in the [motivation](#motivation), using modular routes we now have\n\n```ruby\n# routes.rb\n\nmodular_routes do\n  resources :articles do\n    collection do\n      post :stats\n    end\n\n    member do\n      post :archive\n    end\n  end\nend\n```\n\nor to be shorter\n\n```ruby\n# routes.rb\n\nmodular_routes do\n  resources :articles do\n    post :stats,   on: :collection\n    post :archive, on: :member\n  end\nend\n```\n\nThe output routes for the code above would be\n\n| HTTP Verb | Path                  | Controller#Action     | Named Route Helper        |\n| --------- | --------------------- | --------------------- | ------------------------- |\n| GET       | /articles             | articles/index#call   | articles_path             |\n| GET       | /articles/new         | articles/new#call     | new_article_path          |\n| POST      | /articles             | articles/create#call  | articles_path             |\n| GET       | /articles/:id         | articles/show#call    | articles_path(:id)        |\n| GET       | /articles/:id/edit    | articles/edit#call    | edit_articles_path(:id)   |\n| PATCH/PUT | /articles/:id         | articles/update#call  | articles_path(:id)        |\n| DELETE    | /articles/:id         | articles/destroy#call | articles_path(:id)        |\n| POST      | /articles/stats       | articles/stats#call   | stats_articles_path       |\n| POST      | /articles/:id/archive | articles/archive#call | archive_article_path(:id) |\n\n### Restricting routes\n\nYou can restrict resource RESTful routes with `:only` and `:except` similar to what you can do in Rails.\n\n```ruby\nmodular_routes do\n  resources :articles, only: [:index, :show]\n\n  resources :comments, except: [:destroy]\nend\n```\n\n### Renaming paths\n\nAs in Rails you can use `:path` to rename route paths.\n\n```ruby\nmodular_routes do\n  resources :articles, path: 'posts'\nend\n```\n\nis going to produce\n\n| HTTP Verb | Path            | Controller#Action     | Named Route Helper     |\n| --------- | --------------- | --------------------- | ---------------------- |\n| GET       | /posts          | articles/index#call   | articles_path          |\n| GET       | /posts/new      | articles/new#call     | new_article_path       |\n| POST      | /posts          | articles/create#call  | articles_path          |\n| GET       | /posts/:id      | articles/show#call    | article_path(:id)      |\n| GET       | /posts/:id/edit | articles/edit#call    | edit_article_path(:id) |\n| PATCH/PUT | /posts/:id      | articles/update#call  | article_path(:id)      |\n| DELETE    | /posts/:id      | articles/destroy#call | article_path(:id)      |\n\n### Nesting\n\nAs of version `0.2.0`, modular routes supports nesting just like Rails.\n\n```ruby\nmodular_routes do\n  resources :books, only: [] do\n    resources :reviews\n  end\nend\n```\n\nThe output routes for that would be\n\n| HTTP Verb | Path                             | Controller#Action          | Named Route Helper         |\n| --------- | -------------------------------- | -------------------------- | -------------------------- |\n| GET       | /books/:book_id/reviews          | books/reviews/index#call   | book_reviews_path          |\n| GET       | /books/:book_id/reviews/new      | books/reviews/new#call     | new_book_review_path       |\n| POST      | /books/:book_id/reviews          | books/reviews/create#call  | book_reviews_path          |\n| GET       | /books/:book_id/reviews/:id      | books/reviews/show#call    | book_review_path(:id)      |\n| GET       | /books/:book_id/reviews/:id/edit | books/reviews/edit#call    | edit_book_review_path(:id) |\n| PATCH/PUT | /books/:book_id/reviews/:id      | books/reviews/update#call  | book_review_path(:id)      |\n| DELETE    | /books/:book_id/reviews/:id      | books/reviews/destroy#call | book_review_path(:id)      |\n\n### Non-resourceful routes (standalone)\n\nSometimes you want to declare a non-resourceful routes and its straightforward without modular routes:\n\n```ruby\n  get :about, to: \"about/show#call\"\n```\n\nEven being pretty simple, with modular routes you can omit the `#call` action like\n\n```ruby\nmodular_routes do\n  get :about, to: \"about#show\"\nend\n```\n\nIt expects `About::IndexController` to exist in `controllers/about/index_controller.rb`.\n\nIf `to` doesn't match `controller#action` pattern, it falls back to Rails default behavior.\n\n### Scope\n\n`scope` falls back to Rails default behavior, so you can use it just like you would do it outside modular routes.\n\n```ruby\nmodular_routes do\n  scope :v1 do\n    resources :books\n  end\n\n  scope module: :v1 do\n    resources :books\n  end\nend\n```\n\nIn this example it recognizes `/v1/books` and `/books` expecting `BooksController` and `V1::BooksController` respectively.\n\n### Namespace\n\nAs `scope`, `namespace` also falls back to Rails default behavior:\n\n```ruby\nmodular_routes do\n  namespace :v1 do\n    resources :books\n  end\nend\n```\n\n| HTTP Verb | Path               | Controller#Action     | Named Route Helper     |\n| --------- | ------------------ | --------------------- | ---------------------- |\n| GET       | /v1/books          | v1/books/index#call   | v1_books_path          |\n| GET       | /v1/books/new      | v1/books/new#call     | new_v1_book_path       |\n| POST      | /v1/books          | v1/books/create#call  | v1_books_path          |\n| GET       | /v1/books/:id      | v1/books/show#call    | v1_book_path(:id)      |\n| GET       | /v1/books/:id/edit | v1/books/edit#call    | edit_v1_book_path(:id) |\n| PATCH/PUT | /v1/books/:id      | v1/books/update#call  | v1_book_path(:id)      |\n| DELETE    | /v1/books/:id      | v1/books/destroy#call | v1_book_path(:id)      |\n\n### Routing concerns\n\nWhen you want to reuse route declarations that are usually associated with a common behavior, you can use concerns declaring blocks like:\n\n```ruby\nconcern :commentable do\n  resource :comments\nend\n\nconcern :activatable do\n  member do\n    put :activate\n    put :deactivate\n  end\nend\n```\n\nTo use it you can pass it through resource(s) options or calling `concerns` helper inside of a resource(s) block:\n\n```ruby\nresources :articles, concerns: :commentable\n\nresources :articles, concerns: [:activatable]\n\n# or\n\nresources :articles, concerns: :activatable do\n  concerns :commentable\nend\n```\n\nThe output of that would be:\n\n| HTTP Verb | Path                                    | Controller#Action              | Named Route Helper                          |\n| --------- | --------------------------------------- | ------------------------------ | ------------------------------------------- |\n| GET       | /articles/:id/activate                  | articles/activate#call         | activate_article_path                       |\n| GET       | /articles/:id/deactivate                | articles/deactivate#call       | deactivate_article_path                     |\n| GET       | /articles/:article_id/comments          | articles/comments/index#call   | article_comments_path(:article_id)          |\n| GET       | /articles/:article_id/comments/new      | articles/comments/new#call     | new_article_comment_path (:article_id)      |\n| POST      | /articles/:article_id/comments          | articles/comments/create#call  | article_comments_path(:article_id)          |\n| GET       | /articles/:article_id/comments/:id      | articles/comments/show#call    | article_comment_path(:article_id, :id)      |\n| GET       | /articles/:article_id/comments/:id/edit | articles/comments/edit#call    | edit_article_comment_path(:article_id, :id) |\n| PATCH/PUT | /articles/:article_id/comments/:id      | articles/comments/update#call  | article_comment_path(:article_id, :id)      |\n| DELETE    | /articles/:article_id/comments/:id      | articles/comments/destroy#call | article_comment_path(:article_id, :id)      |\n\n### API mode\n\nWhen `config.api_only` is set to `true`, `:edit` and `:new` routes won't be applied for resources.\n\n### Limitations\n\n- `constraints` are supported via `scope :constraints` and options\n- `concerns` are not supported inside `modular_routes` block but can be declared outside and used as options\n\nLet us know more limitations by creating a [new issue](https://github.com/vitoravelino/modular_routes/issues/new).\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## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/vitoravelino/modular_routes. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/vitoravelino/modular_routes/blob/master/CODE_OF_CONDUCT.md).\n\n## Code of Conduct\n\nEveryone interacting in the ModularRoutes project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/vitoravelino/modular_routes/blob/master/CODE_OF_CONDUCT.md).\n\n## Licensing\n\nModular Routes is licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/vitoravelino/modular_routes/blob/master/LICENSE) for the full license text.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvitoravelino%2Fmodular_routes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvitoravelino%2Fmodular_routes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvitoravelino%2Fmodular_routes/lists"}