A full-featured Crystal web framework that catches bugs for you, runs incredibly fast, and helps you write code that lasts. https://luckyframework.org

github banner-short

The goal: prevent bugs, forget about most performance issues, and spend more time on code instead of debugging and fixing tests.

In summary, make writing stunning web applications fast, fun, and easy.

Coming from Rails?

Try Lucky

Lucky has a fresh new set of guides that make it easy to get started.

Feel free to say hi or ask questions on our chat room.

Keep up-to-date

Keep up to date by following @luckyframework on Twitter.

What's it look like?

JSON endpoint:

class Api::Users::Show < ApiAction
  route do
    json user_json

  private def user_json
    user = UserQuery.find(user_id)
    {name: user.name, email: user.email}
  • route sets up a route for "/api/users/:user_id" automatically.
  • If you want you can set up custom routes like get "sign_in" for non REST routes.
  • A user_id method is generated because there is a user_id route parameter.
  • Use json to render JSON. Extract serializers for reusable JSON responses.

Database models

# Set up the model
class User < BaseModel
  table :users do
    column last_active_at : Time
    column last_name : String
    column nickname : String?
  • Sets up the columns that you’d like to use, along with their types
  • You can add ? to the type when the column can be nil . Crystal will then help you remember not to call methods on it that won't work.
  • Lucky will set up presence validations for required fields (last_active_at and last_name since they are not marked as nilable).

Querying the database

# Add some methods to help query the database
class UserQuery < User::BaseQuery
  def recently_active

  def sorted_by_last_name

# Query the database
  • User::BaseQuery is automatically generated when you define a model. Inherit from it to customize queries.
  • Set up named scopes with instance methods.
  • Lucky sets up methods for all the columns so that if you mistype a column name it will tell you at compile-time.
  • Use the lower method on a String column to make sure Postgres sorts everything in lowercase.
  • Use gt to get users last active greater than 1 week ago. Lucky has lots of powerful abstractions for creating complex queries, and type specific methods (like lower).

Rendering HTML:

class Users::Index < BrowserAction
  route do
    users = UserQuery.new.sorted_by_last_name
    render IndexPage, users: users

class Users::IndexPage < MainLayout
  needs users : UserQuery

  def content

  private def render_new_user_button
    link "New User", to: Users::New

  private def render_user_list
    ul class: "user-list" do
      @users.each do |user|
        li do
          link user.name, to: Users::Show.with(user.id)
          text " - "
          text user.nickname || "No Nickname"
  • needs users : UserQuery tells the compiler that it must be passed users of the type UserQuery.
  • If you forget to pass something that a page needs, it will let you know at compile time. Fewer bugs and faster debugging.
  • Write tags with Crystal methods. Tags are automatically closed and whitespace is removed.
  • Easily extract named methods since pages are made of regular classes and methods. This makes your HTML pages incredibly easy to read.
  • Link to other pages with ease. Just use the action name: Users::New. Pass params using with: Users::Show.with(user.id). No more trying to remember path helpers and whether the helper is pluralized or not - If you forget to pass a param to a route, Lucky will let you know at compile-time.
  • Since we defined column nickname : String? as nilable, Lucky would fail to compile the page if you just did text user.nickname since it disallows printing nil. So instead we add a fallback "No Nickname". No more accidentally printing empty text in HTML!


You need to make sure to install the Crystal dependencies.

  1. Run shards install
  2. Run crystal spec from the project root.


  1. Fork it ( https://github.com/luckyframework/web/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Install docker and docker-compose: https://docs.docker.com/compose/install/
  4. Run script/setup to build the Docker containers with everything you need.
  5. Make your changes
  6. Make sure specs pass: script/test.
  7. Add a note to the CHANGELOG
  8. Commit your changes (git commit -am 'Add some feature')
  9. Push to the branch (git push origin my-new-feature)
  10. Create a new Pull Request

Run specific tests with script/test <path_to_spec>


Thanks & attributions

  • SessionHandler, CookieHandler and FlashHandler are based on Amber. Thank you to the Amber team!
  • Thanks to Rails for inspiring many of the ideas that are easy to take for granted. Convention over configuration, removing boilerplate, and most importantly - focusing on developer happiness.
  • Thanks to Phoenix, Ecto and Elixir for inspiring Avram's save operations, Lucky's single base actions and pipes, and focusing on helpful error messages.
  • lucky watch based heavily on Sentry. Thanks @samueleaton!

Changes in 0.18

  • Added: support for Crystal 0.31.1
  • Fixed: how accept / content-type headers are handled #869
  • Added: ParamParsingError for when parsing JSON params fails #874
  • Updated: Lucky::BaseHTTPClient #875
  • Updated: shell scripts for POSIX compliance #879
  • Added: date_input, time_input, datetime_input #877
  • Added: support for HTTP PATCH #885
  • Added: abbr HTML tag #886
  • Fixed: missing primary_key and timestamps in generated migrations #888
  • Fixed: pluralize to take any Int #890
  • Fixed: generation of migrations with resource see Commit
  • Rename: Lucky::HttpRespondable to Lucky::RenderableError see Commit
  • Fixed: accepts_format, and a few other mime type issues #896
  • Fixed: default curl requests to server not responding properly #899
  • Rename: handle_error to render in ErrorAction #903
  • Rename: render to html in Actions #905
  • Update: error message when missing type declaration for needs #907
  • Fixed: model generation allowing for non alphanumeric characters #910
  • Updated: make more errors renderable #911
  • Fixed: help messages now display for precompiled tasks #923
  • Updated: default help messages for tasks #923
  • Fixed: issue with precompile tasks running in some directories #924
  • Added: SQL logging see Avram
  • Updated: error message when postgres isn't running see Avram
  • Updated: Box.create_pair allows for setting attributes, and returns instances see Avram
  • Added: ability to clone a query see Avram
  • Fixed: add_belongs_to in alter statement using wrong Int size see Avram
  • Fixed: incorrect error message from SaveOperation updates in 0.17 see Avram
  • Added: between query method see Avram
  • Added: ordering queries by NULLS FIRST and NULLS LAST see Avram
  • Fixed: missing attributes from SaveOperation see Avram
  • Added: db.schema.restore and db.schema.dump tasks see Avram
  • Added: group query method for doing GROUP BY see Avram
  • Updated: SchemaEnforcer see Avram
  • Fixed: issue when calling before in SaveOperation see Avram
  • Added: JWT auth generation for API apps see LuckyCli
  • Updated: Serializers to be smarter with collections see LuckyCli
  • Updated: webpack to ignore node_modules directory see LuckyCli
  • Removed: cli lucky init task args see LuckyCli
  • Added: new lucky init.custom task to take args as init did before.
  • Fixed: lucky init to catch invalid project names properly.
  • Added: support for browser_binary in LuckyFlow see LuckyFlow

v0.17 (2019-08-13)

  • Rename: Avram::BaseForm to Avram::SaveOperation see Avram
  • Rename: Avram::Field to Avram::Attribute see Avram
  • Update: number_to_currency now returns String instead of writing to the view directly. #809
  • Fixed: bug in running build.release task.
  • Update: mounted components render comments to show start and end of component. #817
  • Revert: returning String for highlight helper. #818
  • Update: text helpers that write to the view moved to their own module. #820
  • Rename: fillable to permit_columns. see Avram
  • Added: skip_if option to LogHandler. #824
  • Rename: Lucky::Exposeable to Lucky::Exposable. #827
  • Rename: Lucky::Routeable to Lucky::Routable. #827
  • Added: memoize macro. #832
  • Added: table_for macro. see Avram
  • Added: xml render method for Actions. #838
  • Rename: text render action to plain_text. #838
  • Update: responsive_meta_tag to be flexible. #835
  • Added: Int16#to_param and Int64#to_param.
  • Fixed: append/replace_class with no default. #842
  • Added: multi database support. see Avram
  • Rename: form_name to param_key. see Avram
  • Fixed: 3rd party shards versions. #855
  • Added: JSON support. see Avram
  • Update: calling first ensures proper order by. see Avram
  • Update: specifying primary keys is more explicit now. see Avram
  • Added: custom primary key name support. see Avram
  • Added: column and primary key support for Int16. see Avram
  • Rename: Query.destroy_all to Query.truncate. see Avram
  • Fixed: model inference with table names. see Avram
  • Rename: virtual to attribute. see Avram
  • Rename: VirtualForm to Operation. see Avram
  • Added: support for Array fields. see Avram
  • Rename: association query methods now prefixed with where_. see Avram
  • Added: query method to bulk delete. see Avram
  • Update: association query methods no longer take a block. see Avram
  • Added: support for polymorphic associations. see Avram
  • Added: db.rollback_to task. see Avram
  • Added: db.migrations.status task. see Avram
  • Added: db.verify_connection task. see Avram
  • Fixed: calling lucky -v from a lucky project failed. see CLI
  • Update: name convention for operations to be VerbNoun. see CLI
  • Added: change_type macro for migrations. see Avram

v0.16 (2019-08-03)

  • Added: support for Crystal 0.30.0

v0.15 (2019-06-12)

  • Removed Lucky::Action::Status. Use Crystal's HTTP::Status enum. #769
  • CookieOverflowError is now checked when the cookie is set instead of later in middleware. #761
  • Crystal 0.29.0 support added
  • Rename Lucky::BaseApp to Lucky::BaseAppServer
  • Rename Sentry to LuckySentry
  • Breaking change - Many text helpers now return a String instead of appending to the view (cycle, excerpt, highlight, pluralize, time_ago_in_words, to_sentence, word_wrap) #781
  • Added new asset host option #795
  • Added new secure header modules #735
  • Added fallback routing #731
  • Updated SSL Handler with HSTS option #734
  • Components are now classes instead of modules #714
  • Fixed BaseHTTPClient params #726
  • Fixed passing Symbol for statuses in redirects #730
  • More helpful errors #733, #732

v.0.14 (2019-04-18)

  • Crystal 0.28.0 support added

v0.13 (2019-02-27)

Github statistic:
  • 1570
  • 95
  • 83
  • 42
  • 88
  • about 7 hours ago


MIT License