0.17
No release in over 3 years
Rack compatible HTTP router for Ruby
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 1.5
~> 0.6
~> 10

Runtime

 Project Readme

Hanami::Router

Rack compatible, lightweight, and fast HTTP Router for Ruby and Hanami.

Status

Gem Version CI

Contact

Installation

Add this line to your application's Gemfile:

gem "hanami-router"

And then execute:

$ bundle

Or install it yourself as:

$ gem install hanami-router

Getting Started

Create a file named config.ru

# frozen_string_literal: true
require "hanami/router"

app = Hanami::Router.new do
  get "/", to: ->(env) { [200, {}, ["Welcome to Hanami!"]] }
end

run app

From the shell:

$ bundle exec rackup

Usage

Hanami::Router is designed to work as a standalone framework or within a context of a Hanami application.

For the standalone usage, it supports neat features:

A Beautiful DSL:

Hanami::Router.new do
  root                to: ->(env) { [200, {}, ["Hello"]] }
  get "/lambda",      to: ->(env) { [200, {}, ["World"]] }
  get "/dashboard",   to: Dashboard::Index
  get "/rack-app",    to: RackApp.new

  redirect "/legacy", to: "/"

  mount Api::App, at: "/api"

  scope "admin" do
    get "/users", to: Users::Index
  end
end

Fixed string matching:

Hanami::Router.new do
  get "/hanami", to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
end

String matching with variables:

Hanami::Router.new do
  get "/flowers/:id", to: ->(env) { [200, {}, ["Hello from Flower no. #{ env["router.params"][:id] }!"]] }
end

Variables Constraints:

Hanami::Router.new do
  get "/flowers/:id", id: /\d+/, to: ->(env) { [200, {}, [":id must be a number!"]] }
end

String matching with globbing:

Hanami::Router.new do
  get "/*match", to: ->(env) { [200, {}, ["This is catch all: #{ env["router.params"].inspect }!"]] }
end

String matching with optional tokens:

Hanami::Router.new do
  get "/hanami(.:format)" to: ->(env) { [200, {}, ["You"ve requested #{ env["router.params"][:format] }!"]] }
end

Support for the most common HTTP methods:

endpoint = ->(env) { [200, {}, ["Hello from Hanami!"]] }

Hanami::Router.new do
  get     "/hanami", to: endpoint
  post    "/hanami", to: endpoint
  put     "/hanami", to: endpoint
  patch   "/hanami", to: endpoint
  delete  "/hanami", to: endpoint
  trace   "/hanami", to: endpoint
  options "/hanami", to: endpoint
end

Root:

Hanami::Router.new do
  root to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
end

Redirect:

Hanami::Router.new do
  get "/redirect_destination", to: ->(env) { [200, {}, ["Redirect destination!"]] }
  redirect "/legacy",          to: "/redirect_destination"
  redirect "/learn-more",      to: "https://hanamirb.org/"
  redirect "/chat",            to: URI("xmpp://myapp.net/")
end

Named routes:

router = Hanami::Router.new(scheme: "https", host: "hanamirb.org") do
  get "/hanami", to: ->(env) { [200, {}, ["Hello from Hanami!"]] }, as: :hanami
end

router.path(:hanami) # => "/hanami"
router.url(:hanami)  # => "https://hanamirb.org/hanami"

Scopes:

router = Hanami::Router.new do
  scope "animals" do
    scope "mammals" do
      get "/cats", to: ->(env) { [200, {}, ["Meow!"]] }, as: :cats
    end
  end
end

# and it generates:

router.path(:animals_mammals_cats) # => "/animals/mammals/cats"

Mount Rack applications:

Mounting a Rack application will forward all kind of HTTP requests to the app, when the request path matches the at: path.

Hanami::Router.new do
  mount MyRackApp.new, at: "/foo"
end

Duck typed endpoints:

Everything that responds to #call is invoked as it is:

Hanami::Router.new do
  get "/hanami",     to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
  get "/rack-app",   to: RackApp.new
  get "/method",     to: ActionControllerSubclass.action(:new)
end

Implicit Not Found (404):

router = Hanami::Router.new
router.call(Rack::MockRequest.env_for("/unknown")).status # => 404

Explicit Not Found:

router = Hanami::Router.new(not_found: ->(_) { [499, {}, []]})
router.call(Rack::MockRequest.env_for("/unknown")).status # => 499

Body Parsers

Rack ignores request bodies unless they come from a form submission. If we have a JSON endpoint, the payload isn't available in the params hash:

Rack::Request.new(env).params # => {}

This feature enables body parsing for specific MIME Types. It comes with a built-in JSON parser and allows to pass custom parsers.

JSON Parsing

# frozen_string_literal: true

require "hanami/router"
require "hanami/middleware/body_parser"

app = Hanami::Router.new do
  patch "/books/:id", to: ->(env) { [200, {}, [env["router.params"].inspect]] }
end

use Hanami::Middleware::BodyParser, :json
run app
curl http://localhost:2300/books/1    \
  -H "Content-Type: application/json" \
  -H "Accept: application/json"       \
  -d '{"published":"true"}'           \
  -X PATCH

# => [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]

If the json can't be parsed an exception is raised:

Hanami::Middleware::BodyParser::BodyParsingError
multi_json

If you want to use a different JSON backend, include multi_json in your Gemfile.

Custom Parsers

# frozen_string_literal: true

require "hanami/router"
require "hanami/middleware/body_parser"

# See Hanami::Middleware::BodyParser::Parser
class XmlParser < Hanami::Middleware::BodyParser::Parser
  def mime_types
    ["application/xml", "text/xml"]
  end

  # Parse body and return a Hash
  def parse(body)
    # parse xml
  rescue SomeXmlParsingError => e
    raise Hanami::Middleware::BodyParser::BodyParsingError.new(e)
  end
end

app = Hanami::Router.new do
  patch "/authors/:id", to: ->(env) { [200, {}, [env["router.params"].inspect]] }
end

use Hanami::Middleware::BodyParser, XmlParser
run app
curl http://localhost:2300/authors/1 \
  -H "Content-Type: application/xml" \
  -H "Accept: application/xml"       \
  -d '<name>LG</name>'               \
  -X PATCH

# => [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]

Testing

# frozen_string_literal: true

require "hanami/router"

router = Hanami::Router.new do
  get "/books/:id", to: "books.show", as: :book
end

route = router.recognize("/books/23")
route.verb      # "GET"
route.endpoint  # => "books.show"
route.params    # => {:id=>"23"}
route.routable? # => true

route = router.recognize(:book, id: 23)
route.verb      # "GET"
route.endpoint  # => "books.show"
route.params    # => {:id=>"23"}
route.routable? # => true

route = router.recognize("/books/23", {}, method: :post)
route.verb      # "POST"
route.routable? # => false

Versioning

Hanami::Router uses Semantic Versioning 2.0.0

Contributing

  1. Fork this repo to your account and clone it locally (git clone git@github.com:your-pseudo/your-cloned-repo.git)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Install the dependencies (bundle install)
  4. Run tests, they should all pass (./script/ci)
  5. Make your changes & check that the tests still pass. Add some test cases if needed.
  6. Commit your changes (git commit -am 'Add some feature')
  7. Push to the branch (git push origin my-new-feature)
  8. Create new Pull Request on Github with some context on what you're trying to fix or to improve with this contribution

Thank you for contributing!

Copyright

Copyright © 2014–2025 Hanami Team – Released under MIT License