Skip to main content

Actions

Each of the available actions for an endpoint are defined using the action method. At a minimum, an action definition must have a name and at least one route. It's a good idea to add a description for each action so Praxis can use it when generating documentation. In addition to a description an action can also specify:

  • routing: paths that should map to this action
  • params: the structure of the incoming query string and the parameters you expect to find in it
  • payload: the structure of the incoming request body
  • headers: specific named headers that Praxis should parse and make available to this action
  • responses: type and code of the possible responses the action can generate
  • nodoc!: this action should not be included in documentation. Also any types defined within its payload or parameter blocks will not appear in the generated documentation.

Routing

The routing block defines the way Praxis will map requests to your actions. This DSL accepts one or more entries of the form: HTTP verb, path (with colon encoded capture variables), and options. For example:

action :index do
routing do
get 'blogs'
get '//orgs/:org_id/blogs'
end
end

Praxis has convenience methods for all the HTTP verbs defined in the HTTP/1.1 Specification (OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE and CONNECT) plus PATCH.

Praxis also accepts the 'ANY' verb keyword to indicate that the given route should match for any incoming verb string. Routes with concrete HTTP verbs will always take precedence against 'ANY' verb routes. For instance, take a look at the following simplistic and contrived example:

class Blogs
include Praxis::EndpointDefinition

action :show do
routing { get '/:id' }
description 'Fetch one blog entry'
end
action :other do
routing { any '/:id' }
description 'Do other stuff with non GET verbs'
end
end

In this case an incoming "GET /" request will always invoke the :show action, while others like "POST /" or "PATCH /" will always map the the :other action. Using the 'ANY' verb is mostly a convenience to avoid repeating several routes with the same exact path for an action that needs to respond to all those verbs in the same manner. There is a subtle difference, however, and that is that using 'ANY' will truly accept any incoming HTTP verb string, while listing them in several routes will need to match the specific supported names. For example, an 'ANY' route like the above will be able to match incoming requests like "LINK /" or "UNLINK /" (assuming the Web server supports it).

Remember that Praxis prefixes all your resources' routes with a string based on the name of your enclosing endpoint definition class, in this case '/blogs' since our class is called Blogs. You can, however, override the prefix for a single route by prepending '//' to the path (like in the example above) if you don't want the resource-wide prefix to apply. Alternately, you can provide a special prefix of either '' or '//' in the routing block to clear the prefix for any other paths given.

Note: The above 'resetting' behavior of '//' applies only to any Resource-level route prefixes that may be defined. It will not override an API-wide base_path if one is defined (see Global Api Info).

You can inspect the complete Praxis routing table using praxis routes or bundle exec rake praxis:routes:

$ rake praxis:routes
+---------------------------------------------------------------------------+
| Version | Path | Verb | Resource | Action | implementation |
+---------------------------------------------------------------------------+
| n/a | /blogs | GET | Blogs | index | -n/a- |
| n/a | /orgs/:org_id/blogs | GET | Blogs | index | -n/a- |
+---------------------------------------------------------------------------+

The route command supports the json format parameter (i.e., bundle exec praxis routes json) to retrieve the complete routing table in JSON format instead of the tabular example above.

Route parameters

Routes can also take optional parameters. Any of those options passed to the route will be sent to the underlying routing engine (Mustermann). This makes it possible to use advanced features like wildcards, and extra type matching restrictions. For example, the following route will match any url ending with /do_stuff except if it starts with /special:

action :wildcards do
routing do
get '/*/do_stuff' , except: '/special*'
end
description "Will match '/foo/bar/do_stuff' but not '/special/do_stuff"
params do
# :splat will contain the matching pieces of the wildcards
attribute :splat, Attributor::Collection.of(String)
end
end

Notice in the example above that if we use wildcard operators for our routes, we will also need to declare the :splat parameter in our action definition. This parameter will contain a collection of strings matching every wildcard in our route (and yes, you can have a route with multiple wildcards). If only one wildcard is used, :splat will still be an array, and will contain a single string element in it. See the Mustermann site for more information about pattern types and other supported options.

Params

Praxis allows you to define the expected structure of incoming request parameters in the query string, in the URL itself and in the request body (payload). By doing so, you can let the framework perform basic request validation and coercion of values into their expected types. This is also a key component of the Praxis documentation generator.

In Praxis actions, the params stanza is used to describe incoming parameters that can be found in both the action path (route) or the query string. In case of name conflicts, parameters in the path always take precedence over parameters in the query string.

You can define the expected structure of URL and query string parameters by using the params method with a block. Use the standard Attributor::Struct interface to declare attributes.

For example, if you want to pass a simple boolean query string parameter in your blog index action you could define it like so:

action :index do
routing { get '' }
params do
attribute :force, Attributor::Boolean, default: false
end
end

Payload

Similar to params, you can define the expected structure of the incoming request body using the payload method. As in the case of params, Attributes are optional by default, so mark them as required if they must be present so Praxis can validate them for you.

action :create do
routing { post '' }
payload do
attribute :title, String, required: true
attribute :text, String, required: true
attribute :author do
attribute :id, Integer, required: true
end
attribute :tags, Attributor::Collection.of(String)
end
end

Give that payload definition sending the following request body with an 'application/json' content type will pass validation:

{
"title": "Why I Ditched My Co-Working Space",
"text": "Last summer I tried the start-up dream. I moved into...",
"author": {
"id": 29
}
}

Note that unlike other frameworks like Rails and Sinatra, Praxis explicitly distinguishes payload parameters from URL parameters (path and query string parameters). Be sure not to expect any parameters coming from the request body in the params accessor. Request body parameters will only appear in payload.

Payload inheritance

It is common practice (especially in RESTful APIs) to be able to accept incoming resource payloads that closely match outgoing resource responses receive from the same API. For example, if you get a blog MediaType from a show action, it is nice to easily modify parts of it, and re-POST it to the API to save some changes. To help with this, Praxis will maps any payload attribute definition of any action to its corresponding attribute of the default MediaType of the Resource. By doing that, the designer can define attributes by name, without being required to specify the type and/or options that might exist in the associated MediaType.

In other words: Praxis will inject a :reference parameter to the payload, pointing to the defined default MediaType of the endpoint (refer to MediaType for more info). It is for this reason that the following create action payload definition is enough if the default MediaType of the corresponding Post resource has those same attribute names defined.

action :create do
routing { post '' }
payload do
attribute :title, required: true
attribute :text, required: true
attribute :author
attribute :tags
end
end

Also, know that you can mix and match the inherited attributes with other ones that do not exist in the MediaType. For example, the above payload can also add a new attribute called :hidden which includes its type, description or any other options it requires.

TODO: MultiPart payloads!!

Headers

Action definitions can call out special request headers that Praxis validates and makes available to your actions, just like params and payload. Use the headers method with the attributor interface for hashes to define request header expectations:

action :create do
routing { post '' }
headers do
key "Authorization", String, required: true
end
end

In addition to defining a header key in the standard Hash manner, Praxis also enhances the DSL with a header method that can shortcut the syntax for certain common cases. The header DSL takes a String name, and an optional type or expected value:

  • if no value is passed, the only expectation is that a header with that name is received.
  • if a Class is passed, it is used as the type to coerce the header value to.
  • if a Regexp value is passed, the expectation is that the header value (if exists) matches it
  • if a String value is passed, the expectation is that the incoming header value (if exists) fully matches it.

Note: specifying both header type and value is not supported with the header method. If you need to use a non-String type and validate the contents in some other way, use the standard key method instead.

Any hash-like options provided as the last argument are passed along to the underlying Attributor types. Here are some examples of how to define header expectations:

headers do
# Defining a required header
header "Authorization"
# Which is equivalent to
key "Authorization", String, required: true

# Defining a non-required header that must match a given regexp
header "Authorization", /Secret/
# Which is equivalent to
key "Authorization", String, regexp: /Secret/

# Defining a required header that must be equal to "hello"
header "Authorization", "hello", required: true
# Which is equivalent to
key "Authorization", String, values: ["hello"], required: true

# Define a header that is cast as an Integer
header "Account-Id", Integer
# Which is equivalent to
key "Account-Id", Integer
end

Using the simplified headers syntax can cover most of your typical definitions, while the native Hash syntax allows you to mix and match many more options. Which one to use is up to you. They both can perfectly coexist at the same time.

Responses

All actions must specify the list of responses that they can return. Do this by using the response method and passing a response name, as well as any additional arguments if applicable.

action :create do
routing { post '' }
response :on_a_break
end

Praxis already provides a set of common responses to work with, but an application can register its own custom responses too. Each registered response has a unique name which is the name to use in the call to response.

If the controller for this action can explicitly return any of the common HTTP errors, its endpoint definition for the action must also explicitly list those responses. For example, if the controller for the :show action uses a "404 Not Found" to indicate that a given resource id is not present in the DB, the response :not_found must be defined in its list of responses. Another way to see this requirement is that any response class that any controller action can return, must have its name listed in the allowed responses of its endpoint definition.

For more information, please see Responses.