When we start a new project with Ruby on Rails we have magic (I’ll overuse this word) command rails new end everything happens under the hood. Data structure, basic gems, basic server configuration. We don’t need to think about anything we have the new project, ready to? I’m not sure, ready to what, but let’s say ready to develop or ready to deliver. But in general - magic.

As I already started to compare everything to rails, let’s continue. Rails is gem, with this gem we have also a set of other gems, this is why there is Gemfile there. I also need Gemfile, and I also need a bundler. Otherwise, we have to install all gems manually. I don’t want to describe how bundler works, there are much better articles about it. If someone doesn’t know this is ruby package manager. We can generate an empty gem file with bundler init command, or just create a file.

# frozen_string_literal: true

source "https://rubygems.org"

# gem "rails"

This generator even suggested we should install rails 🙈

The basis of every ruby framework (or API library) is rack. Whatever we choose (rails, sinatra, hanami, grape, roda) rack is everywhere. Similar to bundler, there is no alternative to that (or I don’t know). So for simple hello world, I can also use rack.

bundle add rack --version 3.0.0

now we can put our code in app.rb

require 'rack'

class MyApp
  def call(env)
    status = 200
    headers = { 'content-type' => 'text/plain' }
    body = ['Hello, World!']

    [status, headers, body]
  end
end

This code show what happen in every request, we call something and at the end we have to return status, headers and body.

But if we want to run this code, we need also config.ru

require_relative 'app'

run MyApp.new

You can check in your rails project, it looks similar

I can tweak it a little, and only one route returns 200, otherwise it’ll be 404

require 'rack'
require 'json'

class MyApp
  def call(env)
    req = Rack::Request.new(env)
    if req.path == '/hello'
      status = 200
      headers = { 'content-type' => 'application/json' }
      body = { message: 'Hello, World!' }.to_json
    else
      status = 404
      headers = { 'content-type' => 'application/json' }
      body = { error: 'Not Found' }.to_json
    end

    [status, headers, [body]]
  end
end

But I also want to host frontend

require 'rack'
require 'json'

class MyApp
  def call(env)
    req = Rack::Request.new(env)

    case req.path
    when '/'
      status = 200
      headers = { 'content-type' => 'text/html' }
      body = [File.read('frontend/index.html')]
    when '/hello'
      status = 200
      headers = { 'content-type' => 'application/json' }
      body = [{ message: 'Hello, World!' }.to_json]
    else
      status = 404
      headers = { 'content-type' => 'text/plain' }
      body = ['Not Found']
    end

    [status, headers, body]
  end
end

I created simple frontend file, which read from endpoint hello

<!DOCTYPE html>
<html>
<head>
  <title>Hello Frontend</title>
</head>
<body>
  <h1>Hello Frontend</h1>
  <div id="message"></div>

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      fetch('/hello')
        .then(response => response.json())
        .then(data => {
          document.getElementById('message').textContent = data.message;
        })
        .catch(error => {
          console.error('Error:', error);
          document.getElementById('message').textContent = 'Error retrieving message.';
        });
    });
  </script>
</body>
</html>

If someone follows my steps, can notice every time I made a change, we have to reload the app, otherwise, changes do not affect it. There are solutions for that, but currently, this is not important to me.

Before I finish for today, there is one, very important topic - tests. There are only 3 URLs, but this is the perfect moment to setup tests. In every project, I’m using rspec, and I don’t want to make an exception in this project. There is also rack-test which help us with testing rack api.

# spec/app_spec.rb
require 'rack/test'
require 'json'
require_relative '../app'

RSpec.describe MyApp do
  include Rack::Test::Methods

  def app
    MyApp.new
  end

  describe 'GET /' do
    it 'returns index.html content' do
      get '/'
      expect(last_response).to be_ok
      expect(last_response.header['Content-Type']).to eq('text/html')
      expect(last_response.body).to include('Hello Frontend')
    end
  end

  describe 'GET /hello' do
    it 'returns JSON message' do
      get '/hello'
      expect(last_response).to be_ok
      expect(last_response.header['Content-Type']).to eq('application/json')
      expect(JSON.parse(last_response.body)).to eq({ 'message' => 'Hello, World!' })
    end
  end

  describe 'GET unknown path' do
    it 'returns 404 Not Found' do
      get '/unknown'
      expect(last_response.status).to eq(404)
      expect(last_response.header['Content-Type']).to eq('text/plain')
      expect(last_response.body).to eq('Not Found')
    end
  end
end

These are super simple tests but covered the whole application.

You can check all files on github https://github.com/tobiaszwaszak/track-gear/tree/501ce0503621fa52c9462f89c6271726c39c70c4

I am open to feedback and welcome any questions you may have. My email is listed on this website.