Designing Lucky: Rock Solid Actions & Routing

Paul Smith

Lucky catches bugs, returns responses quickly, and helps you write maintainable code. Lucky’s actions help you to process requests and return responses as reliably and productively as possible.

To make Lucky actions rock solid, we looked at the problems we often experience while building apps.

Problems that we wanted to solve with Lucky

These issues often slow down development or cause embarrassing bugs:

  • Route helpers that require documentation to use and aren’t discoverable for new developers.
  • Incorrect or missing parameters.
  • Using a path that doesn’t have a corresponding route or action.
  • Bloated controllers.
  • Forgetting to render in complex conditionals.
  • Using the wrong HTTP verb for an action.
  • Renaming params or routes and forgetting to change a reference.

Lucky aims to help solve these problems with actions.

Here’s what Lucky actions look like

class Projects::Show < BrowserAction
  action do
    render_text "show project with id: #{id}"
  end
end

This class handles HTTP routing, generating paths, working with params, and sending responses.

One class per action

Originally Lucky was going to use controller classes with different methods for each action, similar to Rails, Phoenix, and many other frameworks.

In the end, Lucky uses one class per action because it allows Lucky to create methods for route params, generate intuitive and type-safe route helpers, and infer routes so you don’t need a separate routes file. It also keeps the file clean and focused, which is a nice side effect.

Let’s dive into how this works in practice.

Keeping things productive by inferring the route

Reliability should not come at the price of productivity. Creating a single class per action means a bit more typing to set up the class, and the creation of another file. This doesn’t take that long, but it’s still extra work to both read and write.

To balance out those slowdowns, Lucky handles routing in the action itself. This means you no longer need to open a routes file, add the route, and switch back to the controller. Instead, Lucky looks at the class name to determine the RESTful route for you. For example Users::Show handles GET requests at "/users/:id" and Users::Delete handles DELETE requests at /users/:id.

This means less typing, less reading, and no switching to route files to figure out what’s going on.

Don’t worry, Lucky can also handle nested resources, namespaces, and custom routes like “/dashboard”. See more in depth examples in the guides.

Intuitive route helpers

Because the action knows which route it handles, it will generate route helpers. So instead of using route helpers like new_admin_projects_path(project.id) in Rails, you’d do this in Lucky: Admin::Projects::New.with(project.id)

You use the name of the action and pass the necessary params. This is really nice in link and redirect helpers:

link "New message in this project", to: Admin::Projects::New.with(project.id)

Lucky will also help guide you toward the right parameters. If you try to pass the wrong params or misspell something, Lucky will let you know:

link "New message in this project", to: Admin::Projects::Messages::New.with(project.id)

# You'll see a compile-time error like this
Expected 2 arguments to Admin::Projects::Messages::New#with

Overloads are:
  Admin::Projects::Messages::New#with(project_id, id)

This may not be as helpful to experienced devs, but it is extra helpful to new developers. It’s easier to remember, and Lucky will help you find the right way to do things.

Never worry about the HTTP verb again

One of the more annoying issues I’ve run into is using the right path, but the wrong HTTP method.

Here’s an example in Rails for deleting a comment:

link_to "Delete", comment_path(@comment)

Can you spot the issue? The path is right, but I forgot to specify the HTTP verb. This is especially confusing for team members that are new to web development or REST.

In Lucky, the HTTP verb is automatically used in links, forms, and buttons. You never have to even think about it. It just works.

# The right verb (delete) is automatically set for you
link "Delete comment", to: Comment::Delete.with(@comment.id)

Catch errors in conditional responses

I was working on a project where we handled the request differently based on a number of conditionals. It looked something like this:

# Simplified for this example
def new
  if user.present? && sso_enabled?
    redirect_to saml_provider_login_url
  elsif user.present? && !sso_enabled?
    flash[:error] = "This email address does not have SSO enabled"
    redirect_to :back
  end
end

Can you spot the error? There is no final else so Rails tries to render the default view for this action. This was not something we tested for or expected so we didn’t add a view and we got a failure in production.

In Lucky, actions must return a response. So if you did something like this in Lucky:

class MyAction < BrowserAction
  action do
    if user.present? && sso_enabled?
      redirect to: SamlProviderLogin::New
    elsif user.present? && !sso_enabled?
      flash.info = "This email address does not have SSO enabled"
      redirect to: SignIns::New
    end
  end
end

Lucky will inform you that there is a problem:

MyAction returned Lucky::Response | Nil, but it must return a Lucky::Response.

Try this...

  ▸ Make sure to use a method like `render`, `redirect`, or `json` at the end of your action.
  ▸ If you are using a conditional, make sure all branches return a Lucky::Response.

This means you need to write fewer tests, and you will have fewer bugs in production.

Automatically generated param methods

Generating routes in the class also allows Lucky to generate methods for the params in the path.

Here’s what Lucky does for a Users::Show action:

class Users::Show < BrowserAction
  action do
    render_text "Find a user with id of: #{id}"
  end
end

Lucky will generate an id method for accessing the id param because it has to be there for the route to match. This means you can be 100% sure that if you call id, the param will be there.

This may not seem that helpful, but it gets nicer when you use more complex routes. For example, if you create a Projects::Messages::Index action, Lucky will create a message_id method. Now, you don’t ever need to worry about a typo in the param name, or accidentally trying to use id (which doesn’t exist for that path because it is an index). It’s a little thing, but it means you get clean looking actions and helpful errors during development.

We think you’ll love Lucky

Check out the guides to get started, or learn more about why Lucky was made.