A Basic Web Application

In this recipe, we’ll take a look at how we can get started with a simple web application with zero dependencies. The tools in the standard library covers most of the important parts of a web application other than a router. For that, we’ll have to write very rudimentary and naive router.

Let’s start with an ECR1 template and write a basic home page:

<!-- file: home.ecr -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Home</title>
</head>
<body>
   <h1>Welcome to the Home page!</h1>
</body>
</html>

And an about page:

<!-- file: about.ecr -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>About</title>
</head>
<body>
   <h1>About Us</h1>
   <p>We use Crystal at work.</p>
</body>
</html>

Let’s define our handlers as classes that include HTTP::Handler2:

# file: handlers.cr
require "http/server"

# HomeHandler is the controller for the route:
# `GET: /`
class HomeHandler
  include HTTP::Handler

  def call(context : HTTP::Server::Context)
    context.response.content_type = "text/html"
    context.response.status_code = 200
    # Respond with an ECR template:
    ECR.embed "home.ecr", context.response
  end
end

# AboutHandler is the controller for the route:
# `GET: /about`
class AboutHandler
  include HTTP::Handler

  def call(context : HTTP::Server::Context)
    context.response.content_type = "text/html"
    context.response.status_code = 200
    # Respond with an ECR template:
    ECR.embed "about.ecr", context.response
  end
end

# ErrorNotFoundHandler is the controller for the case:
# `Error: 404 Page Not Found`
class ErrorNotFoundHandler
  include HTTP::Handler

  def call(context : HTTP::Server::Context)
    context.response.content_type = "text/html"
    context.response.status_code = 404
    # Respond with a plain string:
    context.response.print "No such route as #{context.request.path}"
  end
end

# Router is a simple router that matches a handler based
# on the request path.
class Router
  include HTTP::Handler

  def route(path : String) : HTTP::Handler
    case path
    when "/"
      HomeHandler.new
    when "/about"
      AboutHandler.new
    else
      ErrorNotFoundHandler.new
    end
  end

  def call(context : HTTP::Server::Context)
    handler = route(context.request.path)
    handler.call(context)
  end
end

And finally, let’s create an array of handlers and add a few HTTP handlers from the standard library and our custom router and run the server:

# file: server.cr
require "http/server"
require "./handlers"

HOST = "127.0.0.1"
PORT = 3000

handlers = [] of HTTP::Handler
handlers << HTTP::LogHandler.new
handlers << HTTP::ErrorHandler.new
handlers << HTTP::CompressHandler.new
handlers << Router.new

server = HTTP::Server.new(handlers)
server.bind_tcp HOST, PORT

p "Server starting to listen on http://#{HOST}:#{PORT}"
server.listen

A tiny Makefile to run our example:

# file: Makefile
.PHONY: run
run:
    crystal run server.cr

You can now run the project with make run and your server should be listening. We can then try the following:

curl localhost:3000
#OUTPUT:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Home</title>
</head>
<body>
   <h1>Welcome to the Home page!</h1>
</body>
</html>

curl localhost:3000/about
#OUTPUT:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>About</title>
</head>
<body>
   <h1>About Us</h1>
   <p>We use Crystal at work.</p>
</body>
</html>

curl localhost:3000/gibberish
#OUTPUT:

No such route as /gibberish