Skip to content

Routing

Controllers describe and handle routes. There are three methods for defining routes

  • Strong Paramaters (recommended)
  • Macro DSL
  • Magic methods (legacy, remain for backwards compatibility, inspired by Rails)

Strong Paramaters#

This is the recommended method for routing. It conflates routing with the type casting of paramaters to help ensure program correctness by leveraging the type system. The paramater type casting also applies to filters and exception handlers, where the use of the current routes paramaters is desired.

Verbs#

The following verbs are provided

class Comments < Application
  base "/comments"

  @[AC::Route::GET("/")]
  def index; end

  @[AC::Route::POST("/")]
  def create; end

  @[AC::Route::PUT("/:id")]
  def replace(id : Int64 | String); end

  @[AC::Route::PATCH("/:id")]
  def update(id : Int64); end

  @[AC::Route::DELETE("/:id")]
  def destroy(id : String); end

  # optional id param
  @[AC::Route::OPTIONS("/?:id")]
  def options(id : Int64 | String | Nil); end
end

Defining routes#

Route paramaters, as opposed to query paramaters, are defined as part of the URL path.

  • you may mark parts of the path with a : to have a param with a matching name in the action
  • i.e. /users/:some_user_id will result in a param named some_user_id
  • /projects/:project_id/tasks/:task_id would have a project_id and task_id param generated
  • route paramaters take precedence over any query paramaters with matching names
  • optional paramaters are prefixed with a ?
  • /users/?:some_user_id/groups allows the path segment some_user_id to be optional
  • glob path allows multiple segments (i.e. captures a / value as part of the params)
  • /path/*:path_to_file would match /path/my/file/location.cr, setting param["path_to_file"] # => "my/file/location.cr"

multiple routes can also be applied to a single function

@[AC::Route::GET("/users/:id/groups")]
@[AC::Route::GET("/users/groups")]
def groups(user_id : Int64?); end

however in the example above you could achieve the same thing with a single route @[AC::Route::GET("/users/?:id/groups")]

Extracting paramaters#

Paramter extraction occurs accross the route params, query params and form data

Take the following example:

# This could be an DB ORM etc
class MyModel
  include JSON::Serializable
  include YAML::Serializable

  getter x : Float64
  getter y : Float64
end

@[AC::Route::PATCH("/:id", body: :model)]
def replace(id : Int64, model : MyModel, merge_arrays : Bool = false); end
  • the id is extracted from the route (the function paramater names are matched)
  • merge_arrays is extracted from the query params and set to false if not provided
  • the model being patched is extracted from the body and serialised according to the Content-Type header and matching parser

Customising parsing#

Parsers can be customised to parse types in various different ways

@[AC::Route::GET("/:id", config: {
  time: {format: "%F %:z"},
  degrees: {strict: false},
  id: {base: 16, underscore: true, strict: false}
})]
def replace(id : Int64, degrees : Float64, time : Time); end

There are built in parsers for the following types

You can find the custom options here

Custom paramater parser#

You can implement your own type parsers, they can be any class that implements def convert(raw : String). Initiailizer arguments are the possible customisations.

record Commit, branch : String, commit : String

struct ::ActionController::Route::Param::ConvertCommit
  # i.e. `"master#742887"`
  def convert(raw : String)
    branch, commit = raw.split('#')
    Commit.new(branch, commit)
  end
end

@[AC::Route::GET("/:commit")]
def replace(commit : Commit); end

any converter scoped to ActionController::Route::Param and class name starting with Convert will automatically be converted. If you would like to be more explicit:

record Commit, branch : String, commit : String

struct ConvertCommit
  # i.e. `"master#742887"`
  def convert(raw : String)
    branch, commit = raw.split('#')
    Commit.new(branch, commit)
  end
end

@[AC::Route::GET("/:commit", converters: {
  commit: ConvertCommit
})]
def replace(commit : Commit); end

Response codes#

By default all responses will return 200 OK. The default can be changed and response codes can be mapped to return types

# change the default response code
@[AC::Route::GET("/", status_code: HTTP::Status::ACCEPTED)]
def replace(commit : Commit)
end

# map response codes to return types
@[AC::Route::GET("/", status: {
  Int32 => HTTP::Status::OK,
  String => HTTP::Status::ACCEPTED,
  Float64 => HTTP::Status::CREATED
})]
def replace : Int32 | String | Float64
  case rand(3)
  when 1
    1
  when 2
    0.5
  else
    "wasn't 1 or 2"
  end
end

Macro DSL#

The macro DSL is the basis for all routing in spider-gazelle. This is as close to the metal as you can get.

class MyPhotos < Application
  # ...

  # GET /my_photos/:id/features
  get "/:id/features", :features do
    # e.g. render a list of features detected on the photo
    features = []
    render json: features
  end

  # POST /my_photos/:id/feature
  post "/my_photos/:id/feature", :feature do
    # add a feature to the photo
    head :ok
  end
end

In the example above we have created a new route with the name features defined by the :features symbol. This creates a function called features in the class MyPhotos class that can be used with filters.

Customizing routes#

You might want to define a route that isn't defined by the class name of the controller. This is also required if you would like to define a root route.

class Welcome < Application
  base "/"

  @[AC::Route::GET("/")]
  def index; end
end

Or to define a complex route

class Features < Application
  base "/my_photos/:photo_id/features"

  # GET /my_photos/:photo_id/features/
  @[AC::Route::GET("/")]
  def index; end

  # GET /my_photos/:photo_id/features/:id
  @[AC::Route::GET("/:id")]
  def show; end

  # POST /my_photos/:photo_id/features/
  @[AC::Route::POST("/")]
  def create; end
end

Magic methods, legacy#

In Spider-Gazelle, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to a specific CRUD operation in a database.

class MyPhotos < Application
  # base route defaults to the class name underscored
  # base 'my_photos'

  # GET /my_photos/
  def index; end

  # POST /my_photos/
  def create; end

  # GET /my_photos/:id
  def show; end
end

The methods defined by the MyPhotos class define the routes that will be created. These are the routes that will be auto-generated if you define the appropriate method.

HTTP Verb Path Controller#Action Used for
GET /my_photos/ my_photos#index display a list of all your photos
GET /my_photos/new my_photos#new return an HTML form for creating a new photo
POST /my_photos/ my_photos#create create a new photo
GET /my_photos/:id my_photos#show display a specific photo
GET /my_photos/:id/edit my_photos#edit return an HTML form for editing a photo
PATCH /my_photos/:id my_photos#update update a specific photo (possibly meta data)
PUT /my_photos/:id my_photos#replace replace a specific photo
DELETE /my_photos/:id my_photos#destroy delete a specific photo

Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to eight different actions.

Redirecting to other routes#

Routes are available as class level functions on the controllers. Consider the Features class above.

Features.show("photo_id", "feature_id")
# => "/my_photos/photo_id/features/feature_id" : String

This can be combined with the redirect helper

redirect_to Features.show("photo_id", "feature_id")

Inspecting routes#

To get a complete list of the available routes in your application execute the ./app --routes command in your terminal.