The parameters associated with an incoming request are available via request.params within the action's #handle method.
request.params[:id]
end
end
end
end
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
POSTrequest with anapplication/jsoncontent type).
# GET /books/1
request.params[:id] # => "1"
# GET /books?category=history&page=2
request.params[:category] # => "history"
request.params[:page] # => "2"
# POST /books '{"title": "request body", "author":"json"}', Content-Type application/json
request.params[:title] # => "request body"
request.params[:author] #=> "json"
end
Accessing parameters
Request parameters are referenced by symbols.
request.params[:q]
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.
request.params.dig(:book, :title) # => "Hanami"
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.
# app/actions/books/index.rb
params do
optional(:page).value(:integer)
optional(:per_page).value(:integer)
end
request.params[:page]
request.params[:per_page]
end
end
end
end
end
The schema in the params block specifies the following:
pageandper_pageare both optional parameters- if
pageis present, it must be an integer - if
per_pageis 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:
request.params[:page] # => 1
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.
params do
optional(:page).value(:integer, gteq?: 1)
optional(:per_page).value(:integer, gteq?: 1, lteq?: 100)
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.
# app/actions/books/index.rb
params do
optional(:page).value(:integer, gteq?: 1)
optional(:per_page).value(:integer, gteq?: 1, lteq?: 100)
end
halt 422 unless request.params.valid?
# At this point, we know the params are valid
request.params[:page]
request.params[:per_page]
end
end
end
end
end
Here's a further example, this time for an action to create a user.
# app/actions/users/create.rb
params do
required(:email).filled(:string)
required(:password).filled(:string)
required(:address).hash do
required(:street).filled(:string)
required(:country).filled(:string)
end
end
halt 422 unless request.params.valid?
request.params[:email] # => "alice@example.org"
request.params[:password] # => "secret"
request.params[:address][:country] # => "Italy"
request.params[:admin] # => nil
end
end
end
end
end
The params block in this action specifies that:
email,passwordandaddressparameters are required to be present.addresshasstreetandcountryas nested parameters, which are also required.email,password,streetandcountrymust 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:
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:
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:
# app/actions/users/params/create.rb
params do
required(:email).filled(:string)
required(:password).filled(:string)
required(:address).hash do
required(:street).filled(:string)
required(:country).filled(:string)
end
end
end
end
end
end
end
# app/actions/users/create.rb
params Params::Create
# ...
end
end
end
end
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
Hanami automatically parses request bodies for requests with the following content types:
multipart/form-data(form submissions with file attachments)application/json(JSON requests)application/vnd.api+json(JSON API requests)
If you need to parse different body types, you can declare a new body parser:
# lib/foo_parser.rb
# Your parsing logic here
end
end
Register that body parser directly and it will parse its own declared media types:
# config/app.rb
config.middleware.use :body_parser, FooParser
end
You can also register a body parser to work on additional media types:
config.middleware.use :body_parser, [FooParser: ["application/x-foo"]]
This is particularly useful if you need to activate Hanami's standard JSON parsing for additional content types:
config.middleware.use :body_parser, [json: ["application/scim+json"]]