In this series of tutorials, we will show you how to build a simple JSON API for a Todo items list with Crystal programming language and Onyx Framework.

Tutorial contents

  1. Part 1 — The First Endpoint (this article)
  2. Part 2 — CRUD

💡 Haven’t heard of Crystal yet or not sure whether to use it? You should check the recent Why Crystal blogpost then.

💡 If you stuck, then ask your question in the Gitter room or on Twitter!


Table of contents:

  1. Installing Crystal
  2. Hello, world!
  3. Initializing an application
  4. Adding dependencies
  5. The first endpoint
  6. Creating an Action
  7. Adding a JSON view
  8. Adding query params

Installing Crystal

Crystal must be installed on your computer to run Crystal programs, just like with the most of the other programming languages.

The official documentation has a thorough guide on how to install Crystal – https://crystal-lang.org/reference/installation/, please follow it, check the language is istalled with crystal -v and move to the next step.

Hello, world!

To run a simple Crystal program, create a file named hello_world.cr and put this line within it:

puts "Hello, world!"

Looks familiar, doesn’t it? Now run the program:

> crystal hello_world.cr
Hello, world!

Congratulations, you’ve successfully created and run your first Crystal program!

ICR

You may wonder if Crystal has an alternative to the IRB. Well, yes, with some implications. Check out ICR:

icr(0.27.2) > puts "Hello, world!"
Hello, world!

Initializing an application

New applications are usually initiated with crystal init app appname. Let’s create a new application called “todo-onyx”:

> crystal init app todo-onyx && cd todo-onyx

You can check that everything is OK with this command (it should return nothing):

> crystal src/todo-onyx.cr

For the sake of the tutorial, rename the todo-onyx.cr file to server.cr and put this line within (for now):

puts "I'm the server"
> crystal src/server.cr
I'm the server

The directory should look like this:

.
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── shard.lock
├── shard.yml
├── spec
│   ├── spec_helper.cr
│   └── todo-onyx_spec.cr
├── src
│   └── server.cr
└── .travis.yml

Adding dependencies

It’s time to add some gems shards! As we’re using Onyx Framework, modify your shard.yml file adding dependencies section like this:

targets:
  todo-onyx:
    main: src/todo-onyx.cr

crystal: 0.28.0

license: MIT

dependencies:
  onyx:
    github: onyxframework/onyx
    version: ~> 0.4.0
  onyx-http:
    github: onyxframework/http
    version: ~> 0.8.0

Run shards install afterwards:

> shards install

To learn more about Crystal shards, see the docs.

Now as you have all the needed dependencies, let’s write some real code.

The first endpoint

Let’s make the server respond to the GET / request. Replace the contents of src/server.cr with this code:

require "onyx/http"

Onyx::HTTP.get "/" do |env|
  env.response << "Hello, Onyx!"
end

Onyx::HTTP.listen

Now run the server in a separate terminal:

> crystal src/server.cr
 INFO [19:40:43.668] ⬛ Onyx::HTTP::Server is listening at http://127.0.0.1:5000

And check the endpoint:

> curl http://127.0.0.1:5000
Hello, Onyx!

Marvelous! Now stop the server with Ctrl+C command:

> crystal src/server.cr
 INFO [19:40:43.668] ⬛ Onyx::HTTP::Server is listening at http://127.0.0.1:5000
 INFO [19:41:19.513] [c6ff5cc0]    GET / 200 90μs
^C
 INFO [19:42:11.264] ⬛ Onyx::HTTP::Server is shutting down!

You should restart the server manually every time you make a change.

Creating an Action

In Onyx::HTTP, you can encapsulate endpoints into separate objects. They usually isolate business logic from the rendering layer.

Create a new file at src/endpoints/hello.cr and put the following code into it:

└── src
    ├── endpoints
    │   └── hello.cr
    └── server.cr
struct Endpoints::Hello
  include Onyx::HTTP::Endpoint

  def call
    context.response << "Hello, Onyx!"
  end
end

Modify the src/server.cr file:

require "onyx/http"
require "./endpoints/**"

Onyx::HTTP.get "/", Endpoints::Hello

Onyx::HTTP.listen

Should you check it with curl, the response will be the same, but now we have a endpoint defined as a separate object.

> curl http://127.0.0.1:5000
Hello, Onyx!

Adding a JSON view

Currently our action actually does some rendering. To separate it into another layer, we’ll make use of the Onyx::HTTP views concept.

Views usually don’t know anything about the application logic, all they do is rendering their payload.

Create a new file at src/views/hello.cr:

└── src
    ├── endpoints
    │   └── hello.cr
    ├── server.cr
    └── views
        └── hello.cr
struct Views::Hello
  include Onyx::HTTP::View

  def initialize(@who : String)
  end

  json message: "Hello, #{@who}!"
end

Now we can use this view in our action:

struct Endpoints::Hello
  include Onyx::HTTP::Endpoint

  def call
    return Views::Hello.new("Onyx")
  end
end
require "onyx/http"

require "./views/**"
require "./endpoints/**"

Onyx::HTTP.get "/", Endpoints::Hello

Onyx::HTTP.listen

The response should now return a JSON string:

> curl http://127.0.0.1:5000
{"message":"Hello, Onyx!"}

Adding query params

Onyx::HTTP::Endpoint module has a powerful params DSL which allows to define strongly-typed parameters. Currently the endpoint always returns "Hello, Onyx!". Let’s change it with a dynamic query parameter:

struct Endpoints::Hello
  include Onyx::HTTP::Endpoint

  params do
    query do
      type who : String = "Onyx" # The default value is "Onyx"
    end
  end

  def call
    # At this point, `params.query.who` is guaranteed to be a String
    return Views::Hello.new(params.query.who)
  end
end

From now on, the URL query affects the returning value:

> curl http://127.0.0.1:5000
{"message":"Hello, Onyx!"}
> curl http://127.0.0.1:5000?who=Crystal
{"message":"Hello, Crystal!"}

That’s all for part I of this tutorial. You’ve learned how to create REST API endpoints with separate business and rendering layers. The complete source code for this (and other) part is available at GitHub.

Continue to part 2 →