Skip to main content
Table of contents

Lesson 3: Ruby on the Web

Introduction

Objectives

In this tutorial we are going to look at:

  • using require to access other Ruby modules
  • using Ruby’s Date type
  • using File to read files in Ruby
  • what HTTP is and how it works
  • using Google Chrome’s Developer tools to inspect HTTP requests
  • building a web server with Ruby
  • templating HTML
  • introducing web security

Goal

By the end of this tutorial you will have built this web application:

Apply for a Barking Permit website, showing the form from lesson 1 and the confirmation page

You’ll have a basic understanding of how web applications work, and a good foundation to build on.

Using require to access other Ruby modules

In lesson 2 we used a number of built-in Ruby methods, like rand (to get a random number), .reverse (to reverse a string), gets and puts (to read and write strings from the command line).

These methods are so useful that they live in the core of Ruby, so you can always call them.

Some bits of Ruby are used less often, and these are not included by default. To use these other bits you need to require them. For example, the "date" module lets you work with dates and times, but it isn’t included by default.

Running require("date") includes the date code so you can use it in your script. For example:

irb(main):001:0> require("date")
=> true
irb(main):002:0> Date.today
=> #<Date: 2019-05-30 ((2458633j,0s,0n),+0s,2299161j)>

Working with Dates in Ruby

Ruby’s date type makes doing things like adding days to a date much easier (if you didn’t have a date type you’d need to worry about how many days there are in a month and so on).

irb(main):003:0> one_thousand_days_in_the_future = Date.today + 1000
=> #<Date: 2022-02-23 ((2459633j,0s,0n),+0s,2299161j)>

The .today method gets today’s date, but you can also create dates for other days using .new:

irb(main):004:0> Date.new(2022, 2, 23)
=> #<Date: 2022-02-23 ((2459633j,0s,0n),+0s,2299161j)>

Because different cultures represent dates in different ways (e.g. Japan use 2019-05-30, the USA use 05/30/2019 and the UK use 30/05/2019), there’s a special method for formatting dates as strings called .strftime. It takes a “format” string as an argument specifying how to show the date:

irb(main):005:0> japan_format = Date.today.strftime("%Y-%m-%d")
=> "2019-05-30"
irb(main):006:0> usa_format = Date.today.strftime("%m/%d/%Y")
=> "05/30/2019"
irb(main):007:0> uk_format = Date.today.strftime("%d/%m/%Y")
=> "30/05/2019"

There are lots of placeholders you can use in your date string, including:

  • %Y - the year as a 4 digit number
  • %m - the month as a 2 digit number
  • %d - the day as a 2 digit number
  • %A - the day of the week as a word (e.g. Thursday)

Task 1: What day were you born on?

Using irb and the Date type, work out what day of the week you were born on.

Answer

irb(main):001:0> require("date")
=> true

irb(main):002:0> birthday = Date.new(1988, 1, 20)
=> #<Date: 1988-01-20 ((2447181j,0s,0n),+0s,2299161j)>

irb(main):003:0> birthday.strftime("%A")
=> "Wednesday"

Using File to read files in Ruby

Sometimes it’s nice to be able to store things in files, and read these files using Ruby so we can do something with the contents. Ruby has a File.read method that lets us do this.

File.read takes the path to the file as an argument, and returns the contents of the file as a string.

irb(main):004:0> File.read("/Users/your-username/Desktop/lesson-1-html-and-css/index.html")
=> "<!doctype html>\n<html>...

We can work with the contents using all the string methods (like .reverse) we’ve already seen.

There are lots of other useful methods like .size (which returns how many characters there are in the string), and .sub (which takes two arguments, and substitutes the first occurence of the first string with the second).

irb(main):005:0> "Cats are the best".sub("Cat", "Dog")
=> "Dogs are the best"
irb(main):006:0> "Dogs are the best".size
=> 17

Task 2: count the characters in a file

We created a file in lesson-1-html-and-css/index.html. Use File.read and .size to work out how many characters there are in the file.

In Finder you can copy the path to a directory by right clicking, holding down the option key (), and choosing Copy “Directory” as Pathname. Alternatively, you can drag the folder onto the terminal and it will type the path for you.

Answer

irb(main):006:0> file = File.read("/Users/your-username/Desktop/lesson-1-html-and-css/index.html")
=> "<!doctype html>\n<html>...
irb(main):007:0> file.size
1260

 Introduction to HTTP

What is HTTP?

HTTP stands for HyperText Transfer Protocol.

HTTP describes the format of the communications between your web browser and a “web server” (which is just a kind of computer).

Your browser sends HTTP requests and the server responds with HTTP responses.

For example, when you visit www.gov.uk your browser will send a request a like this:

GET / HTTP/1.1
Host: www.gov.uk

And GOV.UK’s server will respond with something like:

HTTP/1.1 200 OK
Date: Wed, 29 May 2019 12:08:17 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 32897

<!doctype html>
<html>
... snip ...
</html>

In this case the response contains the HTML of the GOV.UK homepage.

Viewing HTTP requests using browser developer tools

Modern browsers come with built in tools that let you see what’s happening under the hood. In Google Chrome on a Mac you can bring these up for any webpage by pressing Command + Option + I (⌘ + ⌥ + I).

Screenshot showing the Google Chrome developer tools on www.gov.uk

The Network tab shows you the requests and responses your browser is sending and receiving.

Task 3: Use the developer tools to look at an HTTP request

Go to any website you like and use the developer tools to look at the first HTTP request it makes.

Answer (screenshots)

Screenshot showing the Google Chrome developer tools network tab Screenshot showing the Google Chrome developer tools focused on a particular request

Building web servers with Ruby

A web server is a computer program that can understand HTTP requests and respond to them with HTTP responses.

In lesson 1 we built a website which sent the same HTML back for every request. By getting Ruby involved in handling the request we can make our website dynamic (so the response is not always the same). We can also perform side effects in response to certain requests (like sending emails, or printing out physical barking permits).

Ruby is a great language to get started with, because it has everything you need to build simple web servers built right in to the language.

First of all, let’s create some files for us to serve.

Task 4: Create files and directories

  • Create a folder called lesson-3-ruby-on-the-web next to your other lesson folders.
  • Inside this folder, create another folder called public, and copy the images folder from lesson 1 inside.

You should end up with a directory structure like:

lesson-3-ruby-on-the-web
└── public
    └── images
        ├── puppy-in-a-hat.jpg
        └── shiba-inu.jpg

Hello WEBrick!

Earlier we require’d "date" so we could do some things with dates which aren’t in the core ruby language. Similarly, we can require a thing called "webrick" which allows us to build HTTP servers.

We can use WEBrick to create a new server by calling the WEBrick::HTTPServer.new method like this:

require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')

This might look a bit confusing because it uses some bits of Ruby we haven’t introduced yet (what are all these colons doing all of a sudden?). Don’t worry about it - at some point you’ll learn about modules, classes, and named arguments and it will all make sense.

The server lets us respond to HTTP requests in any way we like. We can use the .mount_proc method to attach a block of Ruby code to a particular request like this:

server.mount_proc("/home") do |request, response|
  puts("Hello server!")
  response.body = "Hello browser!"
end

.mount_proc is a bit like .times from lesson 2:

10.times do |i|
  puts("#{10 - i} green bottles, hanging on the wall,")
  # ...
end

only instead of running the block 10 times straight away, WEBrick will run the block every time a request is made to the /home path.

The final thing we need to do to make the server work is start it, which we do by calling the .start method after we’ve made all our calls to .mount_proc:

server.start

Task 5: Create a “hello world” server

  • Create a file inside the lesson-3-ruby-on-the-web folder called hello-world-server.rb
  • Copy / paste (or type) the following ruby into hello-world-server.rb
require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')

server.mount_proc("/home") do |request, response|
  puts("Hello server!")
  response.body = "Hello browser!"
end

server.start
  • On the command line, change directory into lesson-3-ruby-on-the-web using cd
  • Run hello-world-server.rb from the command line with ruby hello-world-server.rb
  • Visit http://localhost:8080/home in your browser
  • Reload the page a few times to see what happens

Answer

You should see a web page that says “Hello browser!” and you should see some lines on the command line like this:

[2019-05-29 21:38:46] INFO  WEBrick 1.3.1
[2019-05-29 21:38:46] INFO  ruby 2.3.7 (2018-03-28) [universal.x86_64-darwin17]
[2019-05-29 21:38:46] INFO  WEBrick::HTTPServer#start: pid=86541 port=8080
Hello server!
::1 - - [29/May/2019:21:38:58 BST] "GET / HTTP/1.1" 200 14
- -> /
Hello server!
::1 - - [29/May/2019:21:38:59 BST] "GET /favicon.ico HTTP/1.1" 200 14
http://localhost:8080/ -> /favicon.ico
Hello server!
::1 - - [29/May/2019:21:39:03 BST] "GET / HTTP/1.1" 200 14
- -> /
Hello server!
::1 - - [29/May/2019:21:39:03 BST] "GET /favicon.ico HTTP/1.1" 200 14
http://localhost:8080/ -> /favicon.ico

Note that puts("Hello server!") still prints to the command line, and that the response.body is what we see in the browser.

Serving HTML

We usually want to respond with some HTML, not just “Hello browser!”.

To make the browser treat our response as HTML we need to set an HTTP header called Content-Type like this:

server.mount_proc("/home") do |request, response|
  response.content_type = "text/html; charset=utf-8"
  response.body = "<h1>Hello browser!</h1>"
end

It can get difficult to maintain if we put all our HTML in ruby strings like we did in the example above. It would be much nicer if we could have our HTML in html files and our Ruby in ruby files. Fortunately, we can achieve this with File.read:

server.mount_proc("/home") do |request, response|
  response.content_type = "text/html; charset=utf-8"
  response.body = File.read("index.html")
end

(This assumes there’s a file called index.html in your command line’s working directory)

Task 6: Serve an HTML file with WEBRick

  • Copy lesson-1-html-and-css/index.html into lesson-3-ruby-on-the-web
  • Create a copy of hello-world-server.rb called barking-permit-server.rb
  • Using the example above, make the server respond with the text from index.html
  • Run the server with ruby barking-permit-server.rb and visit http://localhost:8080/home to check it works
  • When you’re done, stop the server with CTRL + C

Answer

require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')

server.mount_proc("/home") do |request, response|
  response.body = File.read("index.html")
end

server.start

Responding to requests

Our “Apply for a Barking Permit” page contains a form.

Forms let the user provide the server with some input. When you submit the form you should see the URL update to have ?dog-name=Fido&dog-age=3 at the end.

What’s happening is the browser is making a second HTTP request to the server, but this time it’s passing the user input in the URL (the bits after the question mark are called query parameters).

We can access query parameters inside our WEBrick block using request.query["key"] (note: square brackets, not round brackets)

server.mount_proc("/home") do |request, response|

  dog_name = request.query["dog-name"]
  dog_age = request.query["dog-age"]

  puts("The dog #{dog_name} is #{dog_age} years old")

  response.body = File.read("index.html")
end

Task 7: different responses depending on the query

  • Update barking-permit-server.rb to read user input with request.query[]
  • Using if request.query["dog-name"] set the value of response.body to be “The dog Fido is 3 years old” if the user provided a name, or File.read(...) if not
  • Run the server with ruby barking-permit-server.rb, visit http://localhost:8080/home and submit the form
  • When you’re done, stop the server with CTRL + C

Answer

require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')

server.mount_proc("/home") do |request, response|
  dog_name = request.query["dog-name"]
  dog_age = request.query["dog-age"]

  if dog_name
    response.body = "The dog #{dog_name} is #{dog_age} years old"
  else
    response.body = File.read("index.html")
  end
end

server.start

Templating HTML

We’ve learned how to get the user’s form input on the server, and change our response depending on the input.

Our response to the user should be HTML as well though. If we’re reading the HTML response from a file how do we change the contents based on the user’s input?

Templating lets us take a file and substitute placeholders with values of our choice. There are lots of fancy templating languages out there, but for us Ruby’s .sub method will be good enough.

Imagine we had some HTML in a file called fruits.html:

<h1>Favourite Fruit</h1>

<p>My favourite fruit is FAVOURITE_FRUIT_NAME. I like it the best because it is FAVOURITE_FRUIT_REASON.</p>

We could load this file up, and then substitute the placeholders like this:

File.read("fruits.html").
  sub("FAVOURITE_FRUIT_NAME", "Banana").
  sub("FAVOURITE_FRUIT_REASON", "easy to peel")

Which would return:

<h1>Favourite Fruit</h1>

<p>My favourite fruit is Banana. I like it the best because it is easy to peel.</p>

Task 8: Template barking permit

Create a file called barking-permit-template.html in lesson-3-ruby-on-the-web.

Add the following HTML to the file (it’s fine to copy-paste this):

HTML for barking-permit-template.html

<!doctype html>
<html>
  <head>
    <title>Apply for a Barking Permit</title>
    <link rel="stylesheet" href="https://www.govukpaas.app/govuk-frontend.css">
    <style>
    .govuk-barking-permit table {
      font-family: Arial, sans-serif;
      border-collapse: collapse;
      width: 100%;
    }
    .govuk-barking-permit td,th {
      border: 1px solid black;
      text-align: center;
    }
    </style>
  </head>
  <body>
    <div class="govuk-width-container">
      <h1 class="govuk-heading-xl">Apply for a Barking Permit</h1>
      <div class="govuk-grid-row">
        <div class="govuk-grid-column-two-thirds">
          <div class="govuk-panel govuk-panel--confirmation">
            <h1 class="govuk-panel__title">
              Application complete
            </h1>
            <div class="govuk-panel__body">
              Your permit is available below
            </div>
          </div>
          <div class="govuk-barking-permit">
            <table>
              <tr><th colspan="3">Barking Permit</th></tr>
              <tr><td rowspan="6"><img src="images/puppy-in-a-hat.jpg" alt="A puppy in a hat"></td></tr>
              <tr><th>Name</th><td>DOG_NAME</td></tr>
              <tr><th>Age (human years)</th><td>DOG_AGE_HUMAN_YEARS</td></tr>
            </table>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

Update barking-permit-server.rb so that when request.query["dog-name"] is set, response.body is set to File.read("barking-permit-template.html").

Use .sub to replace DOG_NAME with request.query["dog-name"] and a second .sub to replace DOG_AGE_HUMAN_YEARS with request.query["dog-age"].

Run the server with ruby barking-permit-server.rb, visit http://localhost:8080/home and submit the form.

When you’re done, stop the server with CTRL + C.

Answer

require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')

server.mount_proc("/home") do |request, response|
  dog_name = request.query["dog-name"]
  dog_age = request.query["dog-age"]

  if dog_name
    response.body = File.read("barking-permit-template.html").
      sub("DOG_NAME", dog_name).
      sub("DOG_AGE_HUMAN_YEARS", dog_age)
  else
    response.body = File.read("index.html")
  end
end

server.start

Finishing off the Barking Permit

Following Task 8, our Barking Permit has:

  • Name
  • Age (human years)

We could use Ruby to add some more rows to the permit and make it a bit more useful. For example: age in Dog Years (from lesson 2) and how long the permit is valid for.

Task 9: Add rows to the Barking Permit

  • Add rows (<tr>...</tr>) with template placeholders to barking-permit-template.html for:
    • Age (dog years) (DOG_AGE_DOG_YEARS)
    • Issued on (PERMIT_ISSUED_ON)
    • Valid until (PERMIT_VALID_UNTIL)
  • Calculate values for the placeholders using Ruby
    • Age (dog years) should be the Age in human years multiplied by 7 (as per lesson 2)
    • Issued on should be today’s date, formatted as Day/Month/Year
    • Valid until should be today’s date + 1,000 days (arbitrarily), formatted as Day/Month/Year
  • Use three more .subs to replace the placeholders
  • Run the server with ruby barking-permit-server.rb, visit http://localhost:8080/home and submit the form.
  • When you’re done, stop the server with CTRL + C.

Answer

require("webrick")
require("date")

def convert_to_dog_years(age_human_years)
  return age_human_years * 7
end

server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')

server.mount_proc("/home") do |request, response|
  dog_name = request.query["dog-name"]
  dog_age = request.query["dog-age"]

  if dog_name
    response.body = File.read("barking-permit-template.html").
      sub("DOG_NAME", dog_name).
      sub("DOG_AGE_HUMAN_YEARS", dog_age).
      sub("DOG_AGE_DOG_YEARS", convert_to_dog_years(dog_age.to_i).to_s).
      sub("PERMIT_ISSUED_ON", Date.today.strftime("%d/%m/%Y")).
      sub("PERMIT_VALID_UNTIL", (Date.today + 1000).strftime("%d/%m/%Y"))
  else
    response.body = File.read("index.html")
  end
end

server.start

Warning: security!

We’ve deliberately cut some corners by using .sub for our templating which would cause security problems in a production application.

In particular, mixing user input and HTML can leave us vulnerable to a thing called Cross-site scripting.

You don’t need to worry about this for the purposes of this lesson, but if you’re building a production website make sure to talk to a developer to check you’re following good security practices.

Frameworks like Ruby on Rails and Sinatra help you avoid making security mistakes like this.

Congratulations!

If you’ve made it this far, give yourself a massive pat on the back! These three lessons have been a whirlwind introduction to the world of programming on the web. Don’t worry if it things still seem confusing - programming is a skill that takes time to learn.

Programming is amazingly useful now that computers are such a big part of our lives, but it can also be great fun. Hopefully this is just the start of your learning journey!

Further reading