Basic Routing in Crystal HTTP Server


Crystal official website offers a simple example on how to initiate an http server. This post brings it a little further by implementing basic routing functionality.

Fetching Request Path

The idea of routing is based on the requested path. Thus we can make use of request parameter in the block to fetch the parsed request object.

server = HTTP::Server.new(8080) do |request|
    puts request.path
    HTTP::Response.ok("text/plain","Hello World!")
end
server.listen

We start the server ,visit /, and get a string:

"/"

If we visit /app then we get another string:

"/app"

Adding Routes

Now we know that visiting different path will pass in the corresponding pathname, so a Hash would be good to store the path and responding content.

@routes         = {} of String => String
@routes["/"]    = "This is index"
@routes["/app"] = "This is app"

The syxtax is quite self-expressive. We visit / and will get "This is index" string.

Then we check the incoming request path and respond with the content.

server = HTTP::Server.new(port) do |request|
  if @routes.has_key?(request.path.to_s)
    HTTP::Response.ok("text/plain",@routes[request.path.to_s])
  else
    HTTP::Response.not_found
  end
end

And we're done!

Sinatra Style Routing

Let's bring it a little further. If you ever tried any simple web framework like Sintra or Frank, you will be familiar with the following syntax:

get "/" do
  @text = "hello world"
end

This can be achieved by using Proc. Check the comments for instructions in the following file.

require "http/server"

module BasicHttp
  class Base
    def initialize
      # allow @routes to save Proc as its value
      @routes = {} of String => ( -> String)
    end

    def run
      server = HTTP::Server.new(8080) do |request|
        if @routes.has_key?(request.path.to_s)
          # add call method to proc when returned
          HTTP::Response.ok("text/plain",@routes[request.path.to_s].call)
        else
          HTTP::Response.not_found
        end
      end
      server.listen
    end

    # add method to dynamically add routes
    def get(route, &block : ( -> String))
      @routes[route.to_s] = block
    end
  end
end

app = BasicHttp::Base.new

# the app will respond with the returned string
app.get "/" do
  "hello world"
end

# you can also exec code in block and return a string value
app.get "/app" do
  a = "hello"
  b = "world"
  "#{a} #{b}"
end

app.run

At first sight the file may look a little complicated, but in fact we just add several methods to the original example. The whole idea can be summarized as:

  • Add a @routes variables to store paths and responses
  • Add a get method to allow dynamic routing
  • When responding, use call method to execute code block defined by get method
  • Create a new server instance, add routes, and start listening

And we're done!