Dual-booting Rails 7 & Kemal (a Crystal framework)
Tags: crystal, rails, tutorial, ruby
Published: 2022-04-29 13:42:47

View the source for this [on github](https://github.com/crimson-knight/crystal-in-rails).

While working on a start up idea I have an interesting thought occurred to me: could I run Rails and a Crystal framework in parallel?

## The "why" behind the idea
My side project idea revolves around a snippet of JS code that triggers a chat bot interaction, so I have two end points this snippet will interact with as part of this. Every time a page loads with this snippet a request gets sent to my app. This means adding a single customer could mean adding hundreds of thousands of requests per month. My first thought was dual-booting Rails with Sinatra or even Roda. However I really want to adopt Crystal as much as possible because I believe the language has enormous potential, so why not try and create a developer experience that is just like working with two ruby frameworks at the same time? Could I use this as a strangler approach to slowly move individual end-point behavior from Rails into Crystal and get the significant performance boost a compiled language has to offer?

For this article I'm going to walk you through step by step how to get Kemal running alongside your Rails app within the same directory to create a developer experience that feels like you're working on a pure Ruby app!

## Getting started

Here's what we'll be using for this:

- Rails 7 (latest)
- Ruby 3.1
- Crystal 1.4
- Kemal (latest, currently v1.2)
- Nginx (latest provided by brew)
- Nodemon (an NPM package for watching for file changes)
- Foreman (the Ruby gem!)

There's no special compiling requirements for any of these dependencies, so use whatever package manager you have to install each of these. I highly recommend installing Nodemon globally with npm.

### Step 1 Create the Rails app

Let's get our rails app started:

```bash
rails new crystal_rails
cd && bundle install
```

### Step 2 Create the Crystal app

```bash
crystal init app kemal_api
```

This initializes a minimal Crystal application inside of our Rails root. There's no issue here because Rails does not autoload any files outside of the /app and /lib directories. Now we have a few clean up steps to do here.

Let's open the new `kemal_api/shards.yml` file and add this snippet at the end of the file.

```yml
dependencies:
  kemal:
    github: kemalcr/kemal
```

You can delete these extra files in the `kemal_api` directory: - README
- license
- .editorconfig

You'll also need to remove the git repo that was intialized for this folder. Make sure you've `cd`'d into the kemal_api root and run `rm -rf .git` and now your version tracking will be the same for the rails app and this sub-directory.

I recommend copying the contents from the new kemal_api/.gitignore into the .gitingnore file in your root, and modify the paths to be relative to the kemal_api directory like this:

```
/kemal_api/docs/
/kemal_api/lib/
/kemal_api/bin/
/kelam_api/.shards/
/kemal_api/*.dwarf
```

Crystal compiles all of the apps dependencies into the /lib folder and crystal can auto generate documentation from your code into the /docs/ folder, so this will prevent adding hundreds of unnecessary files into version control.

Now let's install the shards for Kemal:
```bash
cd kemal_api
shards install
```
Now you'll see a `shard.lock` file in the crystal apps root directory.

Time to add Kemal and give it a test route. Open the src/kemal_api.cr file and modify it to match:

```crystal
require "kemal"

# TODO: Write documentation for `KemalApi`
module KemalApi
  VERSION = "0.1.0"

  get "/api/v1/test" do
    # This will be the HTML body in the response.
    "Kemal works! This is our API endpoint :)"
  end
  
  Kemal.run
end
```

You'll notice the syntax for declaring a route is nearly identical to Sinatra... because Kemal is basically the Sinatra of Crystal! That's why my mind went to using this framework ;)


## Step 3 Configuring Rails

Now we'll need to make a few small modifications in Rails to get both apps booting up at the same time.

When Rails 7 is configured to use importmaps (the default) you won't have the `bin/dev` command available. That's alright, we'll go ahead and add a Procfile.dev

Back in the rails root, we need to modify or create our Procfile.dev and add a line for the new Kemal app we just setup.

If you are using jsbundling, you'll already have this file and you'll need to modify the `web:` entry and add the `api:` entry. Leave the `css:` entry alone so you can still get your styling! :)

```Procfile
web: rails s -p 3001
api: cd kemal_api && nodemon --exec crystal run src/kemal_api.cr --watch src
```

Next let's add a basic root path page to load for our rails app.

`rails g controller home`

I'm going to keep this path the way it is for now, so we can see error pages and the successful path in both Rails and Kemal.

### Step 4 Configuring Nginx

To have both apps co-exist on the same domain and port, we'll be setting up Nginx to act as a reverse proxy just like you would in a prod environment.

We'll need to edit the default Nginx server block and configs. If you don't know where this file is, you can run `nginx -t` and Nginx will output the file path for you, along with doing a syntax check (very handy!)

Here is how I've setup my local Nginx:
```
worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;

        # Paths to the Kemal application
        location /api/v1/ {
                proxy_pass              http://0.0.0.0:3000$request_uri;
        }

        # Everything else going to Rails
        location / {
                proxy_set_header Host   $http_host;
                proxy_pass              http://0.0.0.0:3001/$request_uri;
        }
    }
}
```

You'll need to restart or reload your Nginx service to get the updated configuration. On a mac with a homebrew installed Nginx this command is `brew services restart nginx`.

### Step 5 Testing our applications

In your terminal you can run `foreman start -f Procfile.dev` and both apps should now boot up.

In your browser go to : http://localhost/home/index and you'll see how auto generated controller view!

Try going to the root path and you'll see the standard rails error page that the route does not exist.

Then visit http://localhost/api/v1/test and you'll see our Kemal message appearing!

Let's make a change in our kemal app by adding a route.

I'm going to add:
```crystal
get "/api/v1/app-restart" do
  "Kemal should now automatically restart"
end
```
As soon as I save the file, I can check the terminal and see the the change was detected and is now being restarted. Excellent! 

**Note** If you have a firewall enabled on Mac, you will have to allow the outside connections every time it restarts. The way around this has been using the `bin/dev` command that comes with Rails when jsbundling is chosen.

#Success!

Now you know how easy it is to start building high performance API's with Crystal right alongside your Rails application. If you found this intriguing please leave a comment! 

If there is enough interest, I'll try to write more about using an ORM within the Crystal app for interacting with your database.