Can You Build a Web Application in Ruby Without Rails? Yes, You Can!

This is the third article in the series on building awesome web applications with Rails. It shows how to build a simple web app in Ruby from scratch, without using Rails.

This is the third article in the series on building awesome web applications with Rails. The first talked about why build for web and use Rails, and the second showed how to set up a development environment for building Rails apps.

However, starting with this post, we are going to take a detour from Rails and try to build a simple web application from scratch, without using an ounce of Rails.

But Why?

I strongly believe that to understand the benefits of any tool, you should try to accomplish the same task without using that tool.

Rails is not different. It's a tool that helps us quickly build web applications.

However, it also abstracts and hides a lot of things from our plain eyes. Hence sometimes it feels like 'magic'. When things go wrong, and they will, it's difficult to find the root cause of the failure.

For this reason, for the next few articles in this series, we will build a web application using only Ruby, without Rails.

Of course, it goes without saying that this is only for learning purposes, to understand the fundamentals of web applications and frameworks. You should be using Rails for your real-world applications.

I hope this series of articles will give you a peek under the hood and explain some of the core concepts like routing, controllers, views, and much more.

Without any further adieu, let's get started. We will set up our project and create a very simple application that you can access in the browser.

Note: I've published the source code for this post on Github. Checkout the repository and switch to the project-setup branch.  

Create a new directory named web.

➜  mkdir web
➜  cd web

Set up Bundler

Bundler makes sure Ruby applications run the same code on every machine.

It does this by managing the gems that the application depends on. Given a list of gems, it can automatically download and install those gems, as well as any other gems needed by the gems that are listed.

Initialize Bundler for your project by running the following commands from the project root directory.

➜  gem install bundler
➜  bundle init

It will create a Gemfile in the project. Whenever we want to use a gem in our project, we will add it to this file, and run bundle install command to install it.

As a shortcut, bundler provides the bundle add command, which adds the gem to the Gemfile and runs the bundle install for us.

The first gem we will install using Bundler is the Puma app server.

Install Puma

To run our web application, we will need an application server.

The purpose of an application server is to take an incoming HTTP request and pass it to our application. Our app will then process it, build the response HTML, and hand it back to the application server.  

Note: This is a simplified diagram. In reality, we will need a web server/reverse proxy such as Nginx that sits in front of Puma and handles the incoming requests from the web. But let's not worry about that now.

Rails uses the Puma application server. We will use the same.

Install Puma using bundler.

➜  bundle add puma

This will install Puma on your machine, but how will it talk to our application? For that, we need Rack.

Install Rack

All Ruby application servers and web applications follow the Rack specification.

Rack provides a simple interface using which application servers communicate with the applications.

The Rack specification has two conditions:

  • The application should have a call method that takes an env object representing the incoming HTTP request.
  • The application should return an array containing the status, headers, and response.

To learn more about Rack specification, check out this article.

The Definitive Guide to Rack for Rails Developers
This article explains pretty much everything you need to know about Rack as a Ruby and Rails developer. We will start with the basics and move to more advanced concepts like middleware and the Rack DSL.

So that is Rack, a specification. However, in addition to being an abstract protocol, Rack is also a gem. It provides a bunch of useful tools, such as middleware and a convenient DSL (domain-specific language) to run rack-compliant web applications.

Let's Install Rack using Bundler.

➜  bundle add rack

Now, let's create an application Puma will talk to using the Rack specification.

Create a Web Application

Create `application.rb`, our application. Yes, IT IS our entire app.

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/html'
    response = ['<h1>Hello World!</h1>']
    [200, headers, response]

As you can see, it follows the Rack protocol.

  1. It has a call method that takes an env object.
  2. It returns an array containing the status, headers, and response.

The Final Piece of the Puzzle

At this point, we have the Puma application server, and we also have our application app.rb.

The only thing missing is the glue that connects the two.

How does Puma pass the incoming request to our application?

The answer is: a rackup (.ru) file.

When we launch Puma (next step) without any arguments, it looks for this rackup file in the current directory called It's a convention.

So let's add a file in the web directory.

➜  touch

Add the following code to this file. It loads the rack gem and also loads our application.

require 'rack'
require_relative './app'


The run method is provided by the Rack gem that we installed earlier. It takes a Ruby object as an argument and invokes the call method on it.

Since the App class has a call method, Rack can run it just fine.

We have the final piece of the puzzle. The only thing remaining is running the application.

Run the Application

To launch the web server, run the puma command from the web directory.

➜  web git:(main) ✗ puma
Puma starting in single mode...
* Puma version: 6.0.0 (ruby 3.1.0-p0) ("Sunflower")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 6041
* Listening on
Use Ctrl-C to stop

Now our web server is running and serving our application. Open your browser and go to URL.

Congratulations! You have the simplest web application written in Ruby.

Reloading Application

At this point, we have a small problem.

If you make a change in your application, it won't be reflected in the browser. For this, you need to restart the server by pressing ctrl + c on the keyboard. After making a change, restart the server by running rackup again. Your change will show up now.

It can get tedious to restart the server after every change.

Is there a better way?

Yes! The Rack gem ships with a Rack::Reloader middleware that reloads the application after changing the source code.

A middleware is a small, focused, and reusable application that provides useful functionality to your main app.

Middleware sits between the user and the application code. When an HTTP request comes in, the middleware can intercept, examine, and modify it. Similarly, it can examine and modify the HTTP response before forwarding it to the user.

Check out my article on Rack middleware to learn more.

Use the Reloader

Add the Rack::Reloader middleware in the file.

require 'rack'
require_relative './app'

use Rack::Reloader, 0 # <- Add this line

Now launch the web server using puma command. The Reloader middleware will automatically reload the application when the next request arrives, if the source code was changed. So we don't have to restart the server manually.

Problem solved.

What's Next?

In the next article in this series, we will refactor our application to a more Rails-like structure. Currently, our app is returning a hard-coded string response directly from the Ruby code, which is not ideal. We will learn how to separate the "view" from the application logic and move it to a different part of the codebase.

Stay tuned!!

Note: I've published the source code for this post on Github. Checkout the repository and switch to the project-setup branch.

Subscribe to Akshay's Blog

Sign up now to get access to the library of members-only issues.
Jamie Larson