Skip to content

hanami/router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

f1bb97e · Mar 14, 2025
Jan 1, 2025
Mar 14, 2025
Jan 14, 2022
Mar 14, 2025
Nov 14, 2017
Jul 10, 2018
Dec 31, 2022
Apr 10, 2017
Feb 17, 2024
Nov 8, 2022
Nov 5, 2024
Dec 31, 2022
Jan 19, 2016
Nov 3, 2024
Jun 29, 2023
Mar 14, 2025
Jul 10, 2018
Jan 1, 2025

Repository files navigation

Hanami::Router

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

Status

Gem Version CI Test Coverage Depfu

Contact

Installation

Hanami::Router supports Ruby (MRI) 3.1.+

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 [email protected]: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