Onyx Framework is released!

logo_small

I’m proudly presenting you Onyx Framework – the essence of my two years experience with Crystal!


:link: https://onyxframework.org
:octopus: https://github.com/onyxframework


Introduction :wave:

The framework consists of multiple components:

  • Onyx::HTTP is a collection of HTTP handlers, which essentialy are building blocks for your web application
  • Onyx::REST is a REST layer on top of Onyx::HTTP which implements splitting business and rendering logic into Actions and Views, inspired by Hanami
  • Onyx::SQL is a database-agnostic SQL ORM
  • Onyx/Onyx is a collection of macros to make the development eaiser. They act as DSL to hide the boilerplate code

Onyx Framework is designed to be both powerful and adoptable by Crystal newcomers. It utilizes complex concepts like annotations and generics, but hides it under beautiful DSL. Such an approach makes it possible to write less code, thus reducing the possibility of bugs, but still make it easy to extend the framework’s functionality.

Onyx Framework is built with scalability in mind. It is able to grow with the developer’s knowledge of Crystal and the framework itself. Almost every piece of code prefers composition over inheritance and respects configuration over convention.

You are not forced to use certain components to achieve your goals. For example, you can use Onyx::SQL in your Amber project or mix Onyx::REST with Granite and Crecto. Furthermore, it is quite easy to write custom renderers for Onyx::REST and custom database converters for Onyx::SQL.

Code example :open_book:

Here is an example of a simple JSON blogposts application:

# models/post.cr

class Post
  include Onyx::SQL::Model

  # Define DB mapping for this model
  schema posts do
    pkey id : Int32
    type title : String
    type content : String
    type created_at : Time, default: true
  end
end
# views/posts.cr

struct Views::Posts
  include Onyx::REST::View

  def initialize(@posts : Enumerable(Post))
  end

  # Define the way this view is rendered into JSON
  # It can be achieved in multiple ways, this is just one of them
  json({
    posts: @post.map do |post|
      {
        "id":        post.id,
        "title":     post.title,
        "content":   post.content,
        "createdAt": post.created_at,
      }
    end,
  })
end
# actions/posts/create.cr

struct Actions::Posts::Create
  include Onyx::REST::Action

  # Define type-safe params for this endpoint
  # In this case, we define nested JSON parameters
  params do
    json do
      type post do
        type title : String
        type content : String
      end
    end
  end

  # Define known REST errors with their status codes
  # for this endpoint
  errors do
    type JSONRequired(400)
    type DuplicatedTitle(422)
  end

  def call
    json = params.json
    raise JSONRequired.new unless json

    existing_post = Onyx.query(Post
      .where(title: json.post.title)
      .select(:id)
    ).first?
    raise DuplicatedTitle.new if existing_post

    post = Post.new(
      title: json.post.title,
      content: json.post.content
    )

    Onyx.exec(post.insert)
    status(201)
  end
end
# actions/posts/index.cr

struct Actions::Posts::Index
  include Onyx::REST::Action

  def call
    posts = Onyx.query(Post.all.order_by(:created_at, :desc))
    return Views::Posts.new(posts)
  end
end
# app.cr

require "pg"
require "onyx/rest"
require "onyx/sql"

require "./actions/**/*"
require "./models/**/*"
require "./views/**/*"

Onyx.post "/posts", Actions::Posts::Create
Onyx.get "/posts", Actions::Posts::Index

Onyx.render(:json) # Will render views as JSON
Onyx.listen

Fun facts :nerd_face:

Onyx::REST has a long history from Oct’17 till nowdays. It has been renamed and overhauled multiple times, resulting in 15,890 LOC in additions and 13,947 LOC in deletions. It has been previously known as Prism.

Onyx::SQL once was named Core. And its first version has been released in Sep’17. 23,317 LOC in additions and 17,259 LOC in deletions.

I’ve spent over 900 hours this year coding on Crystal:

Join the community :cookie:

I’ve prepared platforms for productive communication:

The framework relies on functionaily of some of my shards, so there are Gitter rooms for them too:

Next steps :paw_prints:

Releasing Onyx Framework was in my New Year resolutions list as well as updating Crystal World, which now uses Onyx as its foundation :tada:

Now I’m going to write some articles related to Onyx:

  1. A thorough guide to create JSON APIs with Onyx
  2. A multi-part tutorial on how to re-create Crystal World from scratch
  3. A guide on how to create a distributed web socket chat with Onyx utilizing Onyx::EDA, which is WIP Event-Driven Architecture framework

All these articles are going to be both in English and Russian language and I hope to see you among the readers. To never miss it, follow my personal Twitter account – @vladfaust!

16 Likes

Cool,

I am looking forward to the guide on how to create JSON APIs.

1 Like

An example on how accessing request params would be nice.

Also, I would like to ask (although I’m not sure if this is the best place) about if it possible to use HTML view in addition to Text and JSON ones.

Good work!

It’s kinda intuitive:

struct MyAction
  include Onyx::REST::Action

  params do
    path do
      type id : Int32
    end

    query do
      type foo : String?

      nested do
        type bar : String
      end
    end

    json do
      type user do
        type name : String
      end
    end

    form do
      type post do
        type user_id : Int32
        type content : String
      end
    end
  end

  def call
    # Given URL: /users/42?nested[bar]=baz
    #

    pp params.path.id # 42; Can only be Int32

    pp params.query.foo        # nil; Can be either Nil or String
    pp params.query.nested.bar # "baz"; Nested params are non-nilable ATM, see the corresponding issue

    # JSON params can be nil if "Content-Type" header !~ "application/json"
    if json = params.json
      pp json.user.name # Will be String
    end

    # Ditto for form params and "application/x-www-urlformencoded"
    if form = params.form
      pp form.post.user_id # Will be Int32
      pp form.post.content # Will be String
    end
  end
end

I’m working on it. Will be shipped in near days.

Thank you :pray:

Congratulations on the release!

1 Like

Awesome! Be sure to announce this at Crystal [ANN] too.

1 Like

Thanks for your reply!

I see, so you need Onyx::REST for that.
I was trying to get it done with Onyx::HTTP, but now I understood.

Thanks again

Articles incoming: https://blog.onyxframework.org/posts/creating-json-apis-with-onyx-part-1/

1 Like