Check out “Designing Lucky: Rock Solid Actions & Routing” to see how Lucky can make writing your applications reliable and productive with its unique approach to actions and routing.

Routing

Unlike many frameworks, there is no separate routes file. An action declares which route it handles in the class itself.

You can route to an action by using get, put, post and delete macros, or Lucky can figure it out for you.

Let’s generate an index action for showing users with lucky gen.action.browser Users::Index to see what a simple action looks like:

# src/actions/users/index.cr
class Users::Index < BrowserAction
  # GET requests to the /users path are handled by this action
  get "/users" do
    # `text` sends plain/text to the client
    text "List of users goes here"
  end
end

Note lucky gen.action.browser is used to create actions that should be shown in a browser. You can also use lucky gen.action.api for actions meant to be used for a JSON API.

Root page

By default Lucky generates a Home::Index action that handles the root path "/". This is the action that shows you the Lucky welcome page when you first run lucky dev.

Change Home::Index to redirect the user to whatever action you want:

# src/actions/home/index.cr
class Home::Index < BrowserAction
  include Auth::SkipRequireSignIn
  unexpose current_user

  get "/" do
    if current_user?
      # By default signed in users go to the profile page
      # You can redirect them somewhere else if you prefer
      redirect Me::Show
    else
      # Change this to redirect to a different page when not signed in
      render Lucky::WelcomePage
    end
  end
end

It may seem strange to redirect as soon as the users visits “/”, but it comes in handy later on. It makes it easy to redirect to different places depending on who the user is. For example, if a user is an admin you may want to redirect them to the Admin::Dashboard::Show action, and if they’re a regular user you may want to take them to the regular dashboard at Dashboard::Show.

Path parameters

Sometimes you want to name certain parts of the path and access them as parameters.

# src/actions/users/show.cr
class Users::Show < BrowserAction
  get "/users/:my_user_id" do
    text "User with an id of #{my_user_id}"
  end
end

When you start a section of the path with : it will generate method for that param in your action.

In this case anything you pass in the part of the URL for :my_user_id will be available in the my_user_id method. So in this example if you visited /users/123 then the my_user_id would return a text response of User with an id of 123.

You can use as many parameters as you want

Every named parameter will have a method generated for it that so that you can access the value. You can have as many as you want.

For example, delete "/projects/:project_id/tasks/:id" would have a project_id and id method generated on the class for accessing the named parameters.

Query parameters

Other times you may want to accept parameters in the query string, e.g. https://example.com?page=2.

# src/actions/users/index.cr
class Users::Index < BrowserAction
  param page : Int32 = 1

  route do
    text "All users starting on page #{page}"
  end
end

When you add a query parameter with the param macro, it will generate a method for you to access the value. The parameter definition will inspect the given type declaration, so you can easily define required or optional parameters by using non- or nilable types (Int32 vs. Int32?). Parameter defaults are set by assigning a value in the parameter definition. Query parameters are type-safe as well, so when https://example.com?page=unlucky is accessed with the above definition, an exception is raised.

Just like path parameters, you can define as many query parameters as you want. Every query parameter will have a method generated for it to access the value.

Where to put actions

Actions go in src/actions and follow the structure of the class.

For example Users::Show would go in src/actions/users/show.cr and Api::V1::Users::Delete would go in src/actions/api/v1/users/delete.cr

Automatically generate RESTful routes

REST is a way to make access to resources more uniform. It consists of the following actions:

Use the route and nested_route macros to generate RESTful routes automatically based on the class name.

route

For route, it will use the first part of the class name as the resource name, and the second part as one of the resourceful actions listed above.

# Users is the resource
# Show is the RESTful action
class Users::Show < BrowserAction
  # Same as:
  #   get "/users/:id"
  route do
    text "The user with id of #{id}"
  end
end

nested_route

For a nested resource it will use the third to last part as the nested resource name, the second to last part of the class name as the resource name, and the last part as one of the resourceful actions listed above.

# Projects is the parent resource
# Users is the nested resource
# Index is the RESTful action
class Projects::Users::Index < BrowserAction
  # Same as:
  #   get "/projects/:project_id/users"
  nested_route do
    text "Render list of users in project #{project_id}"
  end
end

Namespaces are handled automatically

# Anything before the resource (in this case, `Projects`) will be treated as a namespace
class Admin::Projects::Index < BrowserAction
  # Same as:
  #   get "/admin/projects"
  route do
    text "Render list of projects"
  end
end

Examples of automatically generated routes

For the route macro:

For the nested_route macro:

Path and route helpers

Lucky automatically generates some helpers for generating links.

You can access them as class methods on the action itself. They are:

class Projects::Users::Index < BrowserAction
  # Normally you would use `nested_route`
  # We'll use `get` here to make the example more clear
  get "projects/:project_id/users" do
    text "Users"
  end
end

You can then call these methods

We’ll talk about this more in the Pages guide. You can use the route helper with links and form to automatically set the path and HTTP method at the same time.

Redirecting

You can redirect using the redirect method:

Note that for most methods that link you elsewhere (like redirect, or the link helper in HTML pages) you can pass the action directly if it does not need any params. You can see this in the first redirect example below.

class Users::Create < BrowserAction
  route do
    redirect to: Users::Index # Default status is 302
    redirect to: Users::Show.with(id: "user_id") # If the action needs params
    redirect to: "/somewhere_else" # Redirect using a string path
    redirect to: Users::Index, status: 301 # Override status
  end
end

Run code before or after actions with pipes

Sometimes you want to do things before or after running one or more actions. To do this, you can use pipes. There is a before and after macro for running pipes.

# src/actions/admin/users/index.cr
class Admin::Users::Index < BrowserAction
  before require_admin

  get "/foo" do
    text "List of users"
  end

  private def require_admin
    if current_user.admin?
      continue
    else
      redirect to: SignIns::New
    end
  end

  private def current_user
    # Get the currently signed in user somehow
  end
end

Pipe methods must return continue or return a LuckyWeb::Response by rendering or redirecting. If a LuckyWeb::Response is returned by rendering or redirecting, then no other pipe will run and the action will not be called.

Sharing common logic with mixin modules

This is recommended over class inheritance in most cases. It is more flexible and can be used across action types.

Sometimes logic need to be shared in a more flexible way. Let’s say we want to log certain actions. We may want to log actions in our API or browser-based HTML actions so it doesn’t make sense to extract a base class like LoggedAction Instead we’ll use a LogRequest mixin module:

# src/actions/mixins/log_request.cr
module LogRequest
  macro included
    after log_request_path
  end

  private def log_request_path
    MyCustomLogger.log(request.path)
  end
end

We can then include it in the actions that need it:

# src/actions/dashboard/show.cr
class Dashboard::Show < BrowserAction
  include LogRequest

  get "/dashboard" do
    text "The dashboard"
  end
end

Sharing common logic with inheritance

Sometimes you want to apply a pipe or other code to many actions. To do that you can extract a new type of action. Let’s do that for our admin example above.

# src/actions/admin_action.cr
abstract class AdminAction < BrowserAction
  macro inherited
    before require_admin
  end

  private def require_admin
    if current_user.admin?
      continue
    else
      redirect to: SignIns::New
    end
  end

  private def current_user
    # Get the currently signed in user somehow
  end
end

Now other actions can inherit from AdminAction and will automatically get the require_admin pipe:

# Note we're now inheriting from AdminAction
class Admin::Users::Index < AdminAction
  get "/foo" do
    text "List of users"
  end
end

Cookies and sessions

You can set and access cookies in Lucky like this:

class FooAction < BrowserAction
  get "/foo" do
    cookies["name"] = "Sally"
    cookies["name"] # Will return "Sally"

    session["name"] = "Sally"
    session["name"] # Will return "Sally"
    text "Cookies!"
  end
end

Flash messages

You can show messages using flash. This works similarly to Rails, Phoenix, and other frameworks.

Type safe flash messages

The built in messages types are success, danger and info. Using these will cause compile time errors if you accidentally mistype something. It is recommended to stick to these whenever possible.

# In an action
flash.success = "It worked!"
flash.danger = "That did not work"
flash.info = "Be cool"

These will be rendered by the flash component in src/components/shared/flash_messages.cr. The flash component is called from your MainLayout and GuestLayout (found in src/pages/) by the render_flash method.

You can modify the layout or the component to add HTML classes, change where flash is rendered, etc.

Setting other messages

The built in messages are success, danger and info, but you can use anything you’d like:

flash["something_special"] = "Super spesh"
flash["something_special"] # => "Super spesh"

Clearing and keeping flashes

Sometimes you want to show a flash only for the current request:

# Typically used when calling `render` instead of `redirect`
# That way the message shows for the current request only
flash.now.success = "Woo-hoo!"

Or sometimes you want to keep all messages for the next request:

# Useful when you want to redirect to another page while keeping the flash messages
flash.keep_all
Next: Rendering HTML Pages