Rails 7.1.x - Minimal Rails -- no Database

Run a minimal Rails without a database on a SMALL machine

Overview

I was recently asked how to create a rails site without a database (in order to run on a very small footprint).

I thought that was an interesting question, so here is my exploration. In oder to make the site dynamic, this article creates a landing page and an email form.

Create Rails

# active storage will install even without active record - so we MUST not allow that either

rails new nodb --skip-active-storage --skip-active-record   --css=bootstrap --javascript=esbuild

cd nodb

# be sure it starts without error (no migrations needed) and the default webpage shows up.
bin/dev

Assuming that works lets commit:

git add .
git commit -m 'initial rails no db'

Landing page

Let’s start by replacing the landing page with:

bin/rails g controller Landing index

update the routes with: get 'landing/index' and root "landing#index"

So now the config/routes.rb would look like:

# config/routes.rb
Rails.application.routes.draw do
  get 'landing/index'

  # Defines the root path route ("/")
  root "landing#index"
end

Now if you type bin/rails routes | grep landing you should see:

   Prefix      Verb      URI Pattern             Controller#Action
landing_index  GET  /landing/index(.:format)      landing#index

Let’s test that our new landing page is the new ‘ugly’ page - beautification is left to the reader. Assuming this works lets commit.

git add .
git commit 'created custom landing page'

Add an Email form

So to add a form we need a model and then generate a mailer. Without a database backend we need build a model that acts like and communicates like it has a database - we do this by including ActiveModel components:

Create a contact model

To add a model we can still use the generators and then just make adjustments. We include the desired attributes and types so that the view form will be created (without much effort on our part).

bin/rails g scaffold contact name email subject message:text

Note this will NOT actually create a model, every thing except the model. We will make the model by hand, however, we do need to update the controller - which assumes we will have a persisted state and adds methods we won’t/can’t use.

Update the Controller

Within the controller be sure to remove the set_contact, show, updateand delete methods. All these methods assumes we have a stored contact.id (which we won’t have without a DB)!

because the controller can’t use show: we also need to adjust the create method to redirect to either new, edit or / depending on your preference and users.

So now the controller should look like:

class ContactsController < ApplicationController
  def new
    @contact = Contact.new
  end

  def edit
  end

  def create
    @contact = Contact.new(contact_params)

    respond_to do |format|
      if @contact.valid?
        format.html { redirect_to new_contact_path, notice: "Message was successfully sent." }
        format.json { render :new, status: :created, location: @contact }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @contact.errors, status: :unprocessable_entity }
      end
    end
  end

  private

	# Only allow a list of trusted parameters through.
	def contact_params
		params.require(:contact).permit(:name, :email, :subject, :message)
	end
end

Update the Routes

Now that we have made changes to the controller, let’s update the routes to match the controller.

before trying our new form, by changing resources :contacts to resources :contacts, only: %i[new edit create]

So now the routes should look like:

# config/routes.rb
Rails.application.routes.draw do
  get 'landing/index'
  resources :contacts, only: %i[new edit create]

  # Defines the root path route ("/")
  root "landing#index"
end

now we can run bin/rails routes | grep contacts - we should see the following:

Prefix       Verb URI Pattern                  Controller#Action
contacts     POST /contacts(.:format)          contacts#create
new_contact  GET  /contacts/new(.:format)      contacts#new
edit_contact GET  /contacts/:id/edit(.:format) contacts#edit

Create the Model

Now let’s make the model by hand. Using ‘virtual attributes’ which are usually used in coordination with a normal model, where we want a form to contain additional information, but will not be stored in association with a model.

So we will add a file where models usually go:

# app/models/contact.rb
class Contact
end

Now we need to include several ActiveModel components - to allow us to build our model.

  • ActiveModel::Model - allows the model to work with views, etc.
  • ActiveModel::Attributes - allows the views and the controller to see the attributes (& their types)
  • ActiveModel::Validations - allows validation to be included within the model

So now our model will look like:

# app/models/contact.rb
class Contact
	include ActiveModel
	include ActiveModel::Attributes
	include ActiveModel::Validations
end

Let’s add the attributes and their types that we used in the generator (to create our form). The module ActiveModel::Attributes provides an attribute method which looks like: attribute :attrib_name, :attrib_typ, default: some_value The default: value` part is optional and generally not used, but can be very important to ensure dangerous actions are manually turned on.

So now our code should look like:


# app/models/contact.rb
class Contact
	include ActiveModel
	include ActiveModel::Attributes
	include ActiveModel::Validations

	# Our model attributes to be used by the
	# (note: do not add an `id` since we have no persisted state
	attribute :name, :string
	attribute :email, :string
	attribute :subject, :string
	attribute :message, :string
end

Now since we have included ActiveModel::Validations we can add some validations to ensure we get all the info we need.

We also need to override the persisted? method since this will otherwise try to access the id attribute and we know we cannot ever have a persisted record without a db.

So now, finally, our model should look like:

# app/models/contact.rb
class Contact
	include ActiveModel
	include ActiveModel::Attributes
	include ActiveModel::Validations

	# Our model attributes to be used by the
	# (note: do not add an `id` since we have no persisted state
	attribute :name, :string
	attribute :email, :string
	attribute :subject, :string
	attribute :message, :string

	validates :name, :email, :subject, :message, presence: true

	# since we have no DB (id) we need to override the `persisted?` method - in this case it is always false!
	def persisted? = false
end

Now we can test the form by going to: https://localhost:3000/contacts/new

Assuming this works, let’s commit:

git add .
git commit -m 'added a contact model, controller and view (using virtual attributes)'

Configure the Mail Settings

generally you should configure mail not send in dev and test mode and to contact a ’true’ mail provide in production mode. We will just use defaults and fake emails that can’t be sent. There are several good tools to help with this, mailhog, letteropener, etc.

In Rails 8 all but production mode should be setup by default.

Create a Mailer

Let’s use the generator:

bin/rails generate mailer Contact

you can see this generated a few files:

create  app/mailers/contact_mailer.rb
create    app/views/contact_mailer

Let’s update these to match our needs.

Let’s start with the mailer:

class ContactMailer < ApplicationMailer
end

Let’s add some information and a method to invoke:

class ContactMailer < ApplicationMailer
  default from: 'rails_nodb@example.com'

  def contact_email
    @contact = params[:contact]
    mail(to: 'contact_rep@example.com', subject: @contact.subject)
  end
end

Now that we have that, we can create a view template - given our method is called contact_email will need to name our template similarly: contact_email.html.erb and it might look something like:

<!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <h1>Contact Request Message from NoDB website<h1>
		<h2>Subject: <%= @contact.subject %></h2>
		<h2>From: <%= @contact.name %> (<%= @contact.email %>)</h2>
		<p>has sent the following message:
			<br>
      <%= @contact.message %>.
    </p>
  </body>
</html>

and for safety’s sake (in case the user can’t read the html) we will also make a template called: contact_email.text.erb

Contact Request Message from NoDB website
=========================================

SUBJECT: <%= @contact.subject %>
FROM: <%= @contact.name %> (<%= @contact.email %>)

has sent the following message:

<%= @contact.message %>.

To see a preview start rails and go to: http://localhost:3000/rails/mailers/contact_mailer/contact_email

or to see all mail previews go to: http://localhost:3000/rails/mailers

Now we can test this using:

bin/rails c
contact = Contact.new(
	name: 'Nyima',
	email: 'nyima@example.com',
	subject: 'Hello',
	message: 'Time for dinner'
)
ContactMailer.with(contact: contact).contact_email.deliver_now

you should now see something like:

ContactMailer.with(contact: contact).contact_email.deliver_now
=> #<Mail::Message:17320, Multipart: true, Headers: <Date: Sun, 08 Sep 2024 11:38:17 +0200>, <From: rails_nodb@example.com>, <To: contact_rep@example.com>, <Message-ID: <66dd708962dab_17421f64-3f@GREM-VPJQF2D52M.mail>>, <Subject: Hello>, <Mime-Version: 1.0>, <Content-Type: multipart/alternative; charset=UTF-8; boundary="--==_mimepart_66dd70896202c_17421f64-4c3">, <Content-Transfer-Encoding: 7bit>>

assuming this works lets commit:

git add .
git commit -m "added a contact mailer"

If you are doing any of the mail trapping methods you should be able to read the message.

Integrating the Mailer into the Controller

Normally, I use .deliver_later so that this runs as a background job, but sidekick and most jobs need access to a database, so we will just use: deliver_now (which stops everything within the thread to send the mail), but without database, this is a viable option. So after a valid check add: ContactMailer.with(contact: @contact).contact_email.deliver_now

now the contact controller create method should look like:

# app/controllers/contacts_controller.rb
  # ...
  def create
    @contact = Contact.new(contact_params)

    respond_to do |format|
      if @contact.valid?
			  ContactMailer.with(contact: @contact).contact_email.deliver_now

        format.html { redirect_to new_contact_path, notice: "Message was successfully sent." }
        format.json { render :new, status: :created, location: @contact }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @contact.errors, status: :unprocessable_entity }
      end
    end
  end
	# ...

You can test this by starting rails filling out the form and checking the log files and you should see the email in the logs: http://localhost:3000/contacts/new

and in the logs after submitting you should see:

Started POST "/contacts" for ::1 at 2024-09-08 11:48:27 +0200
Processing by ContactsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "contact"=>{"name"=>"Bill", "email"=>"btihen@gmail.com", "subject"=>"test", "message"=>"message"}, "commit"=>"Create Contact"}
  Rendering layout layouts/mailer.html.erb
  Rendering contact_mailer/contact_email.html.erb within layouts/mailer
  Rendered contact_mailer/contact_email.html.erb within layouts/mailer (Duration: 0.3ms | Allocations: 297)
  Rendered layout layouts/mailer.html.erb (Duration: 0.6ms | Allocations: 558)
  Rendering layout layouts/mailer.text.erb
  Rendering contact_mailer/contact_email.text.erb within layouts/mailer
  Rendered contact_mailer/contact_email.text.erb within layouts/mailer (Duration: 0.1ms | Allocations: 161)
  Rendered layout layouts/mailer.text.erb (Duration: 0.3ms | Allocations: 350)
ContactMailer#contact_email: processed outbound mail in 12.1ms
Delivered mail 66dd72eb8fd29_177c62ea4167ee@GREM-VPJQF2D52M.mail (3.7ms)
Date: Sun, 08 Sep 2024 11:48:27 +0200
From: rails_nodb@example.com
To: contact_rep@example.com
Message-ID: <66dd72eb8fd29_177c62ea4167ee@GREM-VPJQF2D52M.mail>
Subject: test
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_66dd72eb8f7c7_177c62ea416695";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


----==_mimepart_66dd72eb8f7c7_177c62ea416695
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

Contact Request Message from NoDB website
=========================================

SUBJECT: test
FROM: Bill (btihen@gmail.com)

has sent the following message:

message.


----==_mimepart_66dd72eb8f7c7_177c62ea416695
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <h1>Contact Request Message from NoDB website<h1>
                <h2>Subject: test</h2>
                <h2>From: Bill (btihen@gmail.com)</h2>
                <p>has sent the following message:
                        <br>
      message.
    </p>
  </body>
</html>

  </body>
</html>

----==_mimepart_66dd72eb8f7c7_177c62ea416695--

Redirected to http://localhost:3030/
Completed 302 Found in 87ms (Allocations: 58111)


Started GET "/" for ::1 at 2024-09-08 11:48:27 +0200
Processing by ContactsController#new as TURBO_STREAM
  Rendering layout layouts/application.html.erb
  Rendering contacts/new.html.erb within layouts/application
  Rendered contacts/_form.html.erb (Duration: 3.8ms | Allocations: 3976)
  Rendered contacts/new.html.erb within layouts/application (Duration: 4.3ms | Allocations: 4347)
  Rendered layout layouts/application.html.erb (Duration: 29.0ms | Allocations: 20498)
Completed 200 OK in 30ms (Views: 30.0ms | Allocations: 21242)

Assuming that worked lets commit:

git add .
git commit -m "integrated mailer into controller"

Resources

ActiveModel

ActionMailer

ActionMailer Config

Bill Tihen
Bill Tihen
Developer, Data Enthusiast, Educator and Nature’s Friend

very curious – known to explore knownledge and nature