Journey From Node To Crystal

At Duo we've been using Node.js as our server platform of choice for a number of years. Recently we have been playing with and becoming increasingly fond of a very new, not quite complete language, Crystal. Below I'd like to outline the strengths and weaknesses of these two platforms and explain why we're increasingly moving our server based development in favour of Crystal.

Why Node?

The Theory.

We moved to node a number of years ago. We're only a small company with a handful of developers and we needed our staff to be as flexible as possible. Allowing developers to specialise in front or back end was a luxury which wasn't really feasible. If we had a glut of back or front end work, we needed people who could help out, whatever their leanings.

Node seamed an obvious solution to this. Employing developers who knew javascript meant both front and back end environments would be familiar, The tooling, syntax and dependencies would overlap and everyone would get better at javascript because every task would involve javascript.

The Reality.

Back and front end code have very different purposes and require knowledge of very different techniques. Typically front end code deals with user interaction, updating the view and requesting data from the server. Our developers typically work with webpack or browserify to bundle their code, develop their interfaces in React and use CSS frameworks to simplify interface layouts.

On the backend developers deal with running sql queries against a database, utilising ORM's, read and writing files and linking with third party apis. The data flow within the server works in a request response cycle. Between the request all the tasks needed to service the response and need to be carried out in a specific way. If one step relies on the previous step completing, those process need to be done in sequence, if not, they can be done in parallel. 

Async By Default

Node is designed to do every task asynchronously. This mean by default if you ask it to do 5 tasks it'll try and do all of them at the same time. Over the past few years, the solution to this has been to use promises. Without going into too much detail, promises allow the programmer to chain together a series of asynchronous tasks into a series of individual steps performed one after another. 

On the server, having parallel tasks as the default seems like a very efficient idea. In reality most of the tasks we perform as developers require data from the previous task. Even when we can perform the tasks in parallel the resources of the system can get quickly exhausted ie. making lots of parallel requests to the database can exhaust your pool of connections and lower the number of concurrent users you can serve. 

During our years using node, the normality became creating promise chains. Half the code written was about turning async tasks into sequential tasks. These chains become difficult to test, debug and understand. Its can become very difficult, just by looking at the code, to see which order the tasks, and various sub-tasks are executed.

Ryan Dahl, creator of Node sums this up quite well, when discuss Node vs Go-lang in an interview

"But the interface that they present to the user is blocking, and I think that that's a nicer programming model, actually. And you can think through what you're doing in many situations more easily if it's blocking. You know, if you have a bunch of serial actions, it's nice to be able to say: do thing A, wait for a response, maybe error out. Do thing B, wait for a response, error out. And in Node, that's more difficult, because you have to jump into another function call."

Dynamic Types

Anyone who programmes in javascript regularly will be familiar with the error "undefined is not an object". This error happens when you're trying to access a method or property on a null variable, which you expect to be an object. It's not enough to follow the data running through your asynchronous code, you also have to track what type of data you have at any point within the applications. Every time your application receives data from one process and passes it to another your application could fail. If you don't allow for any and every possible value your server will throw an error, or worse do something unexpected.

Why Crystal?

While using node I evaluated lots of other languages and platforms including Python, PHP, Ruby and Go-lang. They tended to be either slower than node or not as nice to write. Speed and syntax are both things within a language that you can optimise, but never really improve beyond a certain point. 

Then last year I read an article about this new Crystal language, it's from a new generation of languages that compiles to native code via LLVM. It has a similar syntax to Ruby (whose syntax I quite like) but runs as quick as go-lang (which is fast!).

Crystal is still very young, but I decided to emulate some of the server side portions of our content managements system. This turned out to be pretty awesome. This is what I found:

  • Crystal is quick, in my tests often x2 as quick as Node for my use case.
  • It uses very little memory - typically <5mb vs >200mb per process for Node.
  • It has an excellent standard library, so we only have 12 dependencies in total, compared to Node's 100's 
  • Code looks synchronous by default, it uses an event loop like node, but Fibers are used for concurrency which communicate through channels like go. Making code much easier to follow.
  • Crystal is statically typed so tells you at compile time if you've made any errors.
  • Crystal type system infers types so it's very easy to use as you don't need to use type annotations very often.

I enjoyed writing Crystal so much that we've recreated our whole CMS backend in crystal. Its api compatible with our Node based CMS so websites can be switched with relatively little effort. This is important as Crystal is still young so we need to keep our options open. 

After getting DuoCMS complete in Crystal I needed a way to test it in production, which you're looking at, at the moment. This site is written in crystal. 

What does the code look like?

Below is a slightly simplified version of the controller code in both Crystal and Node to give a comparison. 

A simple controller in Node (using the express framework)

const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const UserService = require('user-service')
app.use(bodyParser.json())

app.get('/', function (req, res) {
  res.send('Hello World!')
})

app.post('/api/users', function (req, res) {
  if(request.body){
    UserService.save(request.body)
    .then(function(){
      res.send('user saved')
    })
    .catch(function(err){
      res.send(err)
    })
  }else{
    res.send("no user provided")
  }	
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})
		

A simple controller in Crystal (using the Kemal framework)

require "kemal"
require "user"
require "user-service"

get "/" do
  "Hello World!"
end

post "/api/users" do |ctx|
  if (json = ctx.request.body)
    user = User.from_json(json)
    UserService.new.save(user)
    "user saved"
  else
    "no user provided"
  end
end

Kemal.run
		

As you can see from the above code, the structure is very similar. But without the need for promises the overall amount of code seems a lot more trimmed down. This tends to be magnified even more when you build a bigger app. The DuoCMS 5 server code is around 15,609 lines of javascript, DuoCMS 6 is closer to 10,186. At this point in time, DuoCMS 6 actually has more features with 30% fewer lines of code and no confusing control flow!

What's Missing From Crystal?

At the time of writing, the developers of Crystal are saying it's Alpha. In reality I've used production level frameworks which are much flakier. At worst I'd say beta. I can understand their caution, suggesting alpha gives them room to change stuff around, break api's etc. I've been using crystal for around a year and have only had a handful of minor breaking changes, I've had far more upgrade issues with React on the front end. It's also worth noting Crystal is written in Crystal so you can contribute fixes to the language and standard library if something does break ( I know I've done it ).

Currently crystals big outstanding features are:

  • Lack of windows support yet ( I don't care, I work on mac and deploy to linux ).
  • Doesn't do true Parallelism yet ( Neither does Node ).
  • Doesn't have Incremental compilation ( Would be nice, our current system take around 8 seconds to compile after a code change ).
  • There aren't many well maintained open source libraries for use with Crystal, but this will evolve as people start using it for serious projects.

For us non of these are deal breakers. I've needed to contribute a few missing features to libraries we use, but that happens in node land too. So all in all I'm pretty happy.

Should I Try Crystal?

Yes! It's really cool. It's nice to write, simple to read and easy to pick up. Oh and the more people who use it and contribute the better it'll get.

See install instructions here - https://crystal-lang.org/docs/installation/ 

If you're asking if you should use it in production, well that's up to you. Personally I think the only way to get something production ready is to try it in production with things that you can afford to fail, then gradually use it on bigger projects. We're not using it on anything with very high traffic or sensitive, we monitor all of our sites and back them up regularly, we also have Node as an understudy just incase Crystal takes ill.

Useful links



More Articles

Contact Us Arrange A Meeting Get A Quote