The parameters associated with an incoming request are available via the #params method on the request object that’s passed to the #handle method of an action when it is invoked.

  1. module Bookshelf
  2. module Actions
  3. module Books
  4. class Show < Bookshelf::Action
  5. def handle(request, response)
  6. request.params[:id]
  7. end
  8. end
  9. end
  10. end
  11. end

Parameter sources

Parameters for a request come from a number of sources:

  • path variables as specified in the route that has matched the request (e.g. /books/:id)
  • the request’s query string (e.g. /books?page=2&per_page=10)
  • the request’s body (for example the JSON-formatted body of a POST request of Content type application/json).
  1. def handle(request, response)
  2. # GET /books/1
  3. request.params[:id] # => "1"
  4. # GET /books?category=history&page=2
  5. request.params[:category] # => "history"
  6. request.params[:page] # => "2"
  7. # POST /books '{"title": "request body", "author":"json"}', Content-Type application/json
  8. request.params[:title] # => "request body"
  9. request.params[:author] #=> "json"
  10. end

Accessing parameters

Request parameters are referenced by symbols.

  1. request.params[:q]
  2. request.params[:book][:title]

Nested parameters can be safely accessed via the #dig method on the params object. This method accepts a list of symbols, where each symbol represents a level in our nested structure. If the :book param above is missing from the request, using #dig avoids a NoMethodError when attempting to access :title.

  1. request.params.dig(:book, :title) # => "Hanami"
  2. request.params.dig(:deeply, :nested, :param) # => nil instead of NoMethodError

Parameter validation

The parameters associated with a web request are untrusted input.

In Hanami actions, params can be validated using a schema specified using a params block.

This validation serves several purposes, including allowlisting (ensuring that only allowable params are extracted from a request) and coercion (converting string parameters to boolean, integer, time and other types).

Let’s take a look at a books index action that accepts two parameters, page and per_page.

  1. # app/actions/books/index.rb
  2. module Bookshelf
  3. module Actions
  4. module Books
  5. class Index < Bookshelf::Action
  6. params do
  7. optional(:page).value(:integer)
  8. optional(:per_page).value(:integer)
  9. end
  10. def handle(request, response)
  11. request.params[:page]
  12. request.params[:per_page]
  13. end
  14. end
  15. end
  16. end
  17. end

The schema in the params block specifies the following:

  • page and per_page are both optional parameters
  • if page is present, it must be an integer
  • if per_page is present, it must be an integer

With this schema in place, a request with a query string of /books?page=1&per_page=10 will result in:

  1. request.params[:page] # => 1
  2. request.params[:per_page] # => 10

Notice that thanks to the defined params schema with types, "1" and "10" are coerced to their integer representations 1 and 10.

Additional rules can be added to apply further constraints. The following params block specifies that, when present, page and per_page must be greater than or equal to 1, and also that per_page must be less than or equal to 100.

  1. params do
  2. optional(:page).value(:integer, gte?: 1)
  3. optional(:per_page).value(:integer, gte?: 1, lteq?: 100)
  4. end

Importantly, now that our params schema is doing more than just type coercion, we need to explicitly check for and handle our parameters being invalid.

The #valid? method on the params allows the action to check the parameters in order halt and return a 422 Unprocessable response.

  1. # app/actions/books/index.rb
  2. module Bookshelf
  3. module Actions
  4. module Books
  5. class Index < Bookshelf::Action
  6. params do
  7. optional(:page).value(:integer, gte?: 1)
  8. optional(:per_page).value(:integer, gte?: 1, lteq?: 100)
  9. end
  10. def handle(request, response)
  11. halt 422 unless request.params.valid?
  12. # At this point, we know the params are valid
  13. request.params[:page]
  14. request.params[:per_page]
  15. end
  16. end
  17. end
  18. end
  19. end

Here’s a further example, this time for an action to create a user.

  1. # app/actions/users/create.rb
  2. module Bookshelf
  3. module Actions
  4. module Users
  5. class Create < Bookshelf::Action
  6. params do
  7. required(:email).filled(:string)
  8. required(:password).filled(:string)
  9. required(:address).hash do
  10. required(:street).filled(:string)
  11. required(:country).filled(:string)
  12. end
  13. end
  14. def handle(request, response)
  15. halt 422 unless request.params.valid?
  16. request.params[:email] # => "alice@example.org"
  17. request.params[:password] # => "secret"
  18. request.params[:address][:country] # => "Italy"
  19. request.params[:admin] # => nil
  20. end
  21. end
  22. end
  23. end
  24. end

The params block in this action specifies that:

  • email, password and address parameters are required to be present.
  • address has street and country as nested parameters, which are also required.
  • email, password, street and country must be filled (non-blank) strings.

The errors associated with a failed parameter validation are available via request.params.errors.

Assuming that the users create action was part of a JSON API, we could render these errors by passing a body when calling halt:

  1. halt 422, {errors: request.params.errors}.to_json unless request.params.valid?

For an empty POST request with an empty address object, this action would render:

  1. {
  2. "errors": {
  3. "email": [
  4. "is missing"
  5. ],
  6. "password": [
  7. "is missing"
  8. ],
  9. "address": {
  10. "street": [
  11. "is missing"
  12. ],
  13. "country": [
  14. "is missing"
  15. ]
  16. }
  17. }
  18. }

Action validations use the dry-validation gem, which provides a powerful DSL for defining schemas.

Consult the dry-validation and dry-schema gems for further documentation.

Using concrete classes

In addition to specifying parameter validations “inline” in a params block, actions can also hand over their validation responsibilities to a separate class.

This makes action validations reusable and easier to test independently of the action.

For example:

  1. # app/actions/users/params/create.rb
  2. module Bookshelf
  3. module Actions
  4. module Users
  5. module Params
  6. class Create < Hanami::Action::Params
  7. params do
  8. required(:email).filled(:string)
  9. required(:password).filled(:string)
  10. required(:address).hash do
  11. required(:street).filled(:string)
  12. required(:country).filled(:string)
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end
  19. end
  1. # app/actions/users/create.rb
  2. module Bookshelf
  3. module Actions
  4. module Users
  5. class Create < Bookshelf::Action
  6. params Params::Create
  7. def handle(request, response)
  8. # ...
  9. end
  10. end
  11. end
  12. end
  13. end

Validations at the HTTP layer

Validating parameters in actions is useful for validating parameter structure, performing parameter coercion and type validations.

More complex domain-specific validations, or validations concerned with things such as uniqueness, however, are usually better performed at layers deeper than your HTTP actions.

For example, verifying that an email address has been provided is something an action parameter validation should reasonably do, but checking that a user with that email address doesn’t already exist is unlikely to be a good responsibility for an HTTP action to have. That validation might instead be performed by a create user operation, which can perform a check against a user store.

Body parsers

Rack ignores request bodies unless they come from a form submission. This means that, if we have a JSON endpoint, the payload isn’t automatically available in params.

  1. # app/actions/users/create.rb
  2. module Bookshelf
  3. module Actions
  4. module Users
  5. class Create < Bookshelf::Action
  6. accept :json
  7. def handle(request, response)
  8. request.params.to_h # => {}
  9. end
  10. end
  11. end
  12. end
  13. end
  1. $ curl http://localhost:2300/books \
  2. -H "Content-Type: application/json" \
  3. -H "Accept: application/json" \
  4. -d '{"book":{"title":"Hanami"}}' \
  5. -X POST

To enable params from JSON request bodies, use Hanami’s body parsing middleware via your app config:

  1. # config/app.rb
  2. class App < Hanami::App
  3. config.middleware.use :body_parser, :json
  4. end

Now params.dig(:book, :title) will return "Hanami".

If there is no suitable body parser for your format in Hanami, you can declare a new one:

  1. # lib/foo_parser.rb
  2. class FooParser
  3. def mime_types
  4. ['application/foo']
  5. end
  6. def parse(body)
  7. # manually parse body
  8. end
  9. end
  1. # config/app.rb
  2. class App < Hanami::App
  3. config.middleware.use :body_parser, FooParser.new
  4. end