This guide covers the basics of implementing a JSON API. If you have any questions about how to use Lucky in more complex ways, hop on our chatroom. We’d be happy to help!

Respond with JSON

To respond with JSON we use the json method in an action:

# in src/actions/api/articles/show.cr
class Api::Articles::Show < ApiAction
  route do
    json({title: "My Post"})
    # Add an optional status code
    json({title: "My Post"}, Status::OK) # or use an integer like `200`
  end
end

Here is a list of all statuses Lucky supports

Create a serializer

You may need to require the src/serializers folder if your project was generated with Lucky CLI 0.7 or earlier. You can do this by adding require "./serializers/**“ after require "./pages/**" in your src/app.cr file.

Serializers help you customize the response, and allow you to share common JSON.

Let’s create one for rendering the JSON for a single article.

# in src/serializers/articles/show_serializer.cr
class Articles::ShowSerializer < Lucky::Serializer
  def initialize(@article : Article)
  end

  def render
    {title: @article.title}
  end
end

# in the action
class Api::Articles::Show < ApiAction
  route do
    article = ArticleQuery.new.find(id)
    json Articles::ShowSerializer.new(article)
  end
end

Handling nested JSON and extra options

Here’s how you can combine JSON for more complex responses. In this example we’ll render a list of articles and the total number of articles:

# in src/serializers/articles/index_serializer.cr
class Articles::IndexSerializer < Lucky::Serializer
  def initialize(@articles : ArticleQuery, @total : Int64)
  end

  def render
    {
      # reuse the existing Articles::ShowSerializer
      articles: @articles.map { |article| ShowSerializer.new(article) },
      total: @total
    }
  end
end

# in src/actions/api/articles/index.cr
class Api::Articles::Index < ApiAction
  route do
    articles = ArticleQuery.new
    total = ArticleQuery.new.count
    json Articles::IndexSerializer.new(articles, total)
  end
end

Saving to the database

Forms automatically know how to handle JSON params. They just need to be formatted in a way LuckyRecord knows how to handle.

Let’s say you have a form called ArticleForm. Lucky will look for the data nested under an article key:

Remember to add fillable {{field name}} or the field will be ignored. In this case, add fillable title to the ArticleForm to allow the title field to be saved.

{
  "article": {
    "title": "A new article"
  }
}

And then save it like you normally would:

class Api::Articles::Create < ApiAction
  route do
    ArticleForm.create(params) do |form, article|
      if article
        json Articles::ShowSerializer.new(article), Status::Created
      else
        head Status::UnprocessableEntity
      end
    end
  end
end

Remember to set the content type to application/json so Lucky knows that it should process the request as JSON.

Handling errors productively

The above example works, but it would be a pain to handle errors like this in every single action where we create something.

Instead we’ll use the Errors::ShowSerializer that is generated with a new Lucky project (src/serializers/errors/show_serializer.cr) to show errors anytime an error fails to save.

# in src/actions/errors/show.cr
class Errors::Show < Lucky::ErrorAction
  def handle_error(error : LuckyRecord::InvalidFormError)
    if json?
      json Errors::ShowSerializer.new(
        message: "Failed to save",
        details: error.message
      ), Status::UnprocessableEntity
    else
      # Show regular error page if this happens with HTML request
      render_error_page status: 500
    end
  end
end

Now that invalid form errors are handled automatically, we can switch to using create! in our action:

class Api::Articles::Create < ApiAction
  route do
    # create! will raise if the params are invalid
    # The InvalidFormError will be caught and handled automatically
    article = ArticleForm.create!(params)
    json ShowJSON.new(article), Status::Created
  end
end

Processing JSON that isn’t saved to the database

You can use JSON.mapping and JSON.parse to parse the response body in any way you’d like.

Sending empty responses

Sometimes you just need to return a status code. For that we use the head method:

# inside an action
head Status::Created
# or use an integer
head 201
Next: Generating Test Data with Boxes