HTTP Router for Crystal

The most common thing that I do with Crystal macros is to write some way of generating a case statement. If you’re writing something that does networking or deserialisation, translating data into method calls is very common, and automatically generating the code to do it is neat.

I’ve been adding some more HTTP endpoints to my container status board, which has meant that my previously-simple routing logic started to get a bit messy, so it was time to write a macro. I did look at awesome-crystal to see if there was an existing library that I could use. There are two—router.cr and orion—but neither are quite what I’m looking for. They more closely match the Rails-style routing DSL, whereas I just wanted something that would dispatch between methods on an HTTP::Handler class.

The resulting macro isn’t very big, and all it does is generate a call(context) method for you with a single case statement in it. It means you can do this:

class MyHander
  include HTTP::Handler
  include HTTP::Router

  @[HTTP::Route(path: "/stuff")]
  @[HTTP::Route(path: "/stuff", method: :HEAD)]
  def get_stuff(context)
    context.ok_json(result: "success!")
  end

  @[HTTP::Route(path: "/stuff", method: :POST)]
  def post_stuff(context)
    contents = context.request.body.get_to_end
    puts contents
    context.ok_json(result: "success!")
  end
end

And not have to manually write this out:

def call(context : HTTP::Server::Context)
  req = context.request
  case { req.method, req.path }
  when { "GET", "/stuff" }
    self.get_stuff(context)
  when { "HEAD", "/stuff" }
    self.get_stuff(context)
  when { "POST", "/stuff" }
    self.post_stuff(context)
  else
    call_next context
  end
end

You can have a look at it on Codeberg.