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.