Controllers

Controllers turn a matched route into a response. Each action is an instance method on a class that subclasses Altair::Controller, called once per request with the request's params. Routes reference actions with typed references (to: PostsController.index), so a wrong action name is a compile error, not a runtime 500.

class PostsController < ApplicationController
  def index : Nil
    posts = Post.all.to_a
    render :index, locals: {posts: posts}
  end

  def show : Nil
    if post = Post.find(params.fetch("id", Int32))
      render :show, locals: {post: post}
    else
      render text: "Post not found", status: ::HTTP::Status::NOT_FOUND
    end
  end
end

Parameters

params merges the route parameters, the query string and the form body, with route values taking precedence. Fetch with a type to keep actions compile-time safe — a malformed value becomes a 422, never a 500:

params["title"]?          # String? — absent key is nil
params["id"]              # String  — absent key raises KeyError
params.fetch("id", Int32)     # Integer, or 422 on bad input
params.fetch?("id", Int32)    # Integer?, or nil on bad input
params.permit("title", "body")
params.require("post").permit("title", "body")

An application/json request body joins the same params: request.json holds the parsed body, and its top-level scalar values are merged (query and form values win on conflicts):

request.json            # JSON::Any? — nil when the body is not JSON
params["name"]?         # a top-level JSON value, as a String

request.format reports the format the client asked for — the path suffix (/posts.json), else the Accept header, else :html:

case request.format
when :json then render json: post
when :text then render text: "…"
else            render :show
end

Rendering

An action ends by rendering something. Plain strings and helpers:

render html: "<h1>Hello</h1>"
render text: "plain", status: ::HTTP::Status::NOT_FOUND
render json: %({"ok": true})
render json: {ok: true, id: 42}   # any JSON-able object is serialized
redirect_to posts_path
redirect_back fallback: posts_path   # honors Referer, same-host only
head ::HTTP::Status::NO_CONTENT      # bodyless answer; later writes ignored
no_content                           # shorthand for 204

When the controller declares templates, render takes the action name plus its locals:

render :index, locals: {posts: Post.all.to_a}
render :index, layout: false, locals: {posts: Post.all.to_a}
render "form", locals: {post: post}   # a partial, returns a String

Callbacks

Filters run around the action. only: / except: restrict the actions a filter applies to, and a before callback that writes a response (render, redirect, head) halts the chain — the action and its after callbacks are skipped:

class Admin::PostsController < PostsController
  before_action :require_login, only: [:new, :create]
  after_action :audit_action

  skip_before_action :require_login   # inherited filters can be removed
end

Filters are inherited across the controller hierarchy, and a skip declared in one subclass does not affect its siblings. A filter is a plain public method; any response it writes answers the request.

respond_to

One action, several format handlers. The block declares a handler per format; the one matching request.format runs, and a request for an undeclared format answers 406:

def show : Nil
  post = Post.find(params.fetch("id", Int32))
  respond_to do |format|
    format.html { render :show, locals: {post: post} }
    format.json { render json: post }
    format.text { render text: post.to_s }
  end
end

Streaming

stream opens a chunked response body — every write to the yielded IO reaches the client as it happens, with the content type set first:

def events : Nil
  stream("text/event-stream") do |io|
    io << "data: hello\n\n"
    io.flush
  end
end

Exceptions to responses

A controller can map a raised exception to a response. The handler method receives the exception (cast to the registered type), and subclass exceptions match:

class PostsController < Altair::Controller
  rescue_from MissingPost, handle_with: :render_missing

  def render_missing(e : MissingPost) : Nil
    render text: "No such post", status: ::HTTP::Status::NOT_FOUND
  end
end

only: / except: restrict the actions a handler answers for, handlers inherit across the hierarchy, and an unhandled exception re-raises to the application's error pages. At the application level, rescue_from maps an exception to a fixed status:

class Blog < Altair::Application
  rescue_from KeyError, to: 404
end

Templates

The templates macro declares the view files and compiles them into typed render methods — a wrong local name or type is a compile error:

class PostsController < ApplicationController
  templates "posts",
    root: __DIR__ + "/../views",
    layout: "application",
    index: {posts: Array(Post)},
    show: {post: Post}
end

This renders views/posts/index.ecr and views/posts/show.ecr, each receiving exactly the declared locals. See the Views guide.