Skip to main content

Controllers

Controllers are the entrypoint where the implementation to fulfill all API requests live. They behave much in the same as they do in other MVC-based web frameworks - they implement the actions that receive requests from API clients, operate on the underlying resources to service those requests, and return the appropriate responses. In short, controllers are the glue which connects actions and their responses to application business logic.

Praxis controllers differ from some other frameworks in that they:

  • are plain Ruby classes that happen to include the Praxis::Controller module. By using plain Ruby classes, Praxis allows you to use the full power of Ruby. You are not limited in your inheritance options, and your controller code can be well-isolated and easily tested.
  • serve actions by implementing an instance method with the same name as the action in your endpoint, which accept Ruby keyword parameters corresponding to the attribute names of the action definition. Exposing the common parameters as keword method arguments, make your code and interface cleaner, and connects to the API definition in a more direct way.

Including the Controller concern

To implement a controller in Praxis, include the Praxis::Controller module in your controller class and indicate which of your Resource Definitions it implements by using the implements stanza. For example, this could be how you create rthe Post controller class, which will serve the implementation of all actions defined in Endpoints::Posts:

class Posts
include Praxis::Controller

implements Endpoints::Posts

# Controller code...
end

Including the Praxis::Controller module enhances the class with important methods such as implements, lifecycle methods (i.e.,before, around after), request to retrieve the incoming API request object, and response for using the default response object when needed.

Linking to the Endpoint Definition

The implements method is used to connect a controller with its corresponding EndpointDefinition. Technically speaking there is nothing that prevents having the Endpoint definition from also being a controller (i.e., implementing itself). However, and despite that being feasible due to the modularity that Praxis provides, we highly discourage it in order to keep "design" logically separate from "implementation".

Once the controller is linked, you can also get to its corresponding EndpointDefinition object through the definition method, which is defined on both the class and instance.

Implementing an Action

A controller action is an instance method defined on a controller class. The method's name must match an action defined in the controller's resource definition. And its keyword arguments must match the defined parameters in the action.

For example, on the design side, we can have an endpoint definition class for Posts that defines two actions: :index and :show:

class Endpoints::Posts
include Praxis::EndpointDefinition

media_type Post
version '1.0'

action :index do
routing { get '' }
description 'Fetch all blog posts'
end

action :show do
routing { get '/:id' }
description 'Fetch an individual blog post'
params do
attribute :id, Integer, required: true
attribute :token, String, required: true
attribute :allow_deleted, Attributor::Boolean
attribute :extended_info, Attributor::Boolean
end
end
end

The controller implementing this resource definition must have instance methods named index and show must which accept the argument names described by the params block from the resource definition.

class Posts
include Praxis::Controller

implements Endpoints::Posts

def index
# empty method signature: the index action defines no parameters
end

def show(id:, token:, **other_params)
# four parameters defined matching the names of the arguments
# Note that ruby allows is to unpack only the names we care about
# and leave the rest tucked away in the other_params hash
end
end

Note that the index action has no parameters defined in its endpoint definition so the method accepts no arguments.

On the other hand, the show action has four parameters defined in its endpoint, so it can explicitly declare them as named method arguments. Ruby gives you great flexibility in declaring named parameters with the splat operator. It is up to the developer to choose how many explicit arguments to list, and how many to tuck away inside an other_params hash. In this case, the developer decided that id and token are important enough to use as direct variables in the controller (it is common to only explicitly list the required ones), while pushing the allowed_deleted and extended_info into the other_params hash. Having this flexibility is great for dealing with large number of parameters while keeping your controller code tidy.

In addition to using named arguments for incoming parameters, Praxis will also ensure their values match the types that you've specified in the Endpoint Definition. So in this case, you can rest assured that accessing the id variable within the show method will always get you an Integer. In other words, there should be no type and parameter validation being done in your controller, because Praxis ensures that if the code makes it to your action, the parameters (and headers and payloads) will properly be there if they are required, and will have been successfully coerced to the defined types.

Retrieving Headers and Payload Data

While pure path and query string parameters are conveniently exposed as keyword arguments to the function. They are also accessible from inside the request method in the controller (which is provided by the Praxis::Controller include).

In fact, you can not only get to the params structure by accessing request.params, but you can also use the same technique to access the incoming API payload by request.payload and the incoming headers by request.headers. The data retrieved under these methods is type-curated exactly like keyword arguments and is accessible through calling method names matching your attributes. For example, I can access the same exact token query string parameter by calling request.params.token, or retrieve a hypothetical first_name parameter coming from the API payload of a request by request.payload.first_name.

Note that request.headers will only give you the (coreced and validated) headers that you have defined. That is not the function to retrieve all the raw headers that the request carries. The rationale here is that if there are important headers that you're expecting to use in the action, you should probably have defined them in your API endpoint, as that's probably an important information for the API clients to know. For special circumstances, however, you can always access the raw Rack env (which contains the headers) by request.env.

Nil vs not-provided values

Using these Struct-type parameters, you can also test if an incoming value was provided by the client (or has been assigned by a default option). To do so, just use the key? method passing the attribute name. This is useful in those cases where there is an important distinction between a user-provided nil value and the user simply not providing a value, as there is in "PATCH" requests.

Here's a simple made up example of how to access these methods from a controller action:

def update(id:,**other_params)
id == request.params.id # id argument will be the same as request.params.id
accept = request.headers.accept # Retrieve 'Accept' header

if request.payload.key?(:email) # the email attribute was passed (which could be nil)
email = request.payload.email
email ? update_email(email) : reset_email # Update or reset the email
else
# Leave email untouched
end
end

Returning a Response

The final piece of every controller action ia to return a Response object with the right headers, status code and body. To do so, you literally return an instance of a Praxis::Response-derived class from the action, and let Praxis worry about formatting and sending those bits back to the API client.

For every existing HTTP response code, Praxis has an appropriate Response class for you to use. They are all defined under the Praxis::Responses module, using a camel-case named class based on their human http code message. For example, to return a 204 No Content HTTP response, you can simply create and return a NoContent class instance in this way:

 return Praxis::Responses::NoContent.new

These response classes also allow configuration as a way to pass the response payload and its headers. They can be configured on instantiation (by pasing arguments to the controller), or as method calls on the created instance.

Here's an example of how you can create a 201 Created response, with a Location header of /users/23, and a customer header of X-My-Header with value foo. This is done through setting things on the instance:

response = Praxis::Responses::Created.new
response.headers = {
'Location' => '/users/23',
'X-My-Header' => 'foo'
}
return response

And this is the equivalent in doing it through instantiation:

 return Praxis::Responses::Created.new(location: '/users/23', headers: {`X-My-Header` => `foo` })

You are probably wondering why didn't we pass the location through a straight Location header. The answer is that we could have, without problems. We just wanted to illustrate, that since it is so common, we've also allowed you to use the location: parameter to achieve the same. And, in fact, the same is true for a media_type: parameter as well, which would be translated to the appropriate Content-Type parameter of the passed in MediaType object.

Default response object

Many of your API actions are likely return a 200 Ok, at least for the normal execution path. To make things easier keep your controller DRY, Praxis will alway have an instance of a 200 Ok response ready for you in your action. You can access it through the response method, and you can easily change it by setting a different instance of a response at any point in time. Using the precreated response, you can keep your code DRY by not having to create a new one every time you return a 200. Note that this is just a convenience to avoid creating instances for all of the common cases, but in fact, you don't even have to use that response instance at all if you don't want to, as you can always return any response you create from your action.

Here's an example of one way to use and return the default response:

def show(id:, **other_params)
response.headers['Content-Type'] = 'text/plain'
response.body = "This is a simple body"
response
end

Strings as responses

While returning a Response instance from your action is the normal flow, there is also another way. In particular, you can instead return a simple string, and if you do so, Praxis take it as the body to send back to the client, and will make use of the current contents of the response method to complete it. In other words, a string return will tell Praxis that you want to use the current contents of the response object, with the passed string as the body. The response method could be the default 200 instance set by Praxis itself, or could be that you have set it to something else. The final result will set the body of that request to the string returned, and sent that request back to the client. Here's a typical example:

def show(id:, token:, **other_params)
response.headers['Content-Type'] = 'application/json'
'{ "first_name" : "Joe", "email" : "joe@example.com" }'
end

...which essentially results in 1) response.body = <STR> and return response

Any returned value from an action that it is not a Response instance or a String is an immediate error.

Request Life Cycle Callbacks

Including the Praxis::Controller module also provides a way to register one or more callbacks to be executed during the request life cycle. This is done using the before, after and around methods which take zero or more params and a block for Praxis to execute.

For example, to execute a callback before the show action runs, you can add:

before actions: [:show] do
puts "before action"
end

To execute a callback before the validate stage of the request cycle, but only when the action is index, you could add:

before :validate, actions: [:index] do |controller|
puts "About to validate params/headers/payload for action:"
puts "#{controller.request.action.name}"
end

The block receives the instance of your controller, which you can use to access all of the controller's properties, including the request, the response, any actions, etc.

Any of these callbacks are able to interrupt (i.e., shortcut) the execution block of a request by returning an Response instance. For a complete discussion of what stages are available for use in your callbacks, as well as how to use them, please refer to the Request LifeCycle documentation.