REST services in Elixir with Cowboy
cowboy_rest
makes it easy to build REST services with cowboy
.
It is inspired by webmachine1, services in Elixir. In this blog I’ll give a quick example of how to create a couple of simple resources and lastly touch on a few of the downsides of using it.
Where plug might be seen as a way to describe how to do a transformation of the request into the response, cowboy_rest
lets your describe the characteristics of your resource and then let cowboy_rest
decide on how to reply to the request.
In short:
the request is handled as a state machine with many optional callbacks describing the resource and modifying the machine’s behavior
This makes for a nicely declarative way of describing your resources as you’ll hopefully see below. It provides a lot of help (or at least, opinions) in providing correct replies.
You can find the code used in this blog here (alongside parallel implementations in Phoenix
and Webmachine
).
The first resource
Lets start by creating a new Elixir project using the --sup
flag to get a supervisor for our project:
For the rest of this blog I’ll assume you ran the command above and thus named the project cowboy_rest_example
.
Getting :cowboy up and running
We now need to get :cowboy up and running. Open the file containing the start/2
Application callback: lib/cowboy_rest_example.ex
.
Add the following:
The first two arguments :http
and 100
tells cowboy
to listen to http
connections (the other option being :https
) and to start 100 ranch
acceptors. The choice of 100
was more or less arbitrary and I can’t offer much advice as to this setting.
env: [dispatch: dispatch]
sets the dispatcher (aka router) for our resources, we’ll get to that in a moment.
[port: 8000]
tells cowboy to listen to port 8000
.
Dispatching
One of the options we supplied to cowboy
was a dispatch table that we’ll define in CowboyRestExample.Dispatch
:
Routes in cowboy
are defined as a list of {host, [routes]}
tuples. Most likely you’ll want to set the host to :_
meaning match everything. But you could use the host part to match on different subdomains (see: the routing section of the cowboy
documentation for more information).
We’ll start out with a dispatch table that looks like this:
I’ve moved the routes/paths out into the routes
function. Paths are defined as a three element tuple {path_match, handler, options}
.
So in the above example we have told cowboy
to let CowboyRestExample.Hello
handle any requests to "/"
(and we’ve supplied no options to CowboyRestExample.Hello
).
The “Hello, world” handler
Time to write our first handler. Create a file for the CowboyRestExample.Hello
module.
The init
method is the first callback we get from cowboy
when handling requests to "/"
. Here we tell cowboy
that we want to perform a protocol upgrade to :cowboy_rest
. In essence we’re telling cowboy
that our handler will respond to the callbacks used by cowboy_rest
.
As stated in the introduction introduction request will be handled as a state-machine and the callbacks we respond to change the behavior alters the behaviour of that state-machine. But we don’t have to respond to every call! The callbacks all have reasonable defaults.
So for now we can get away with just implementing the to_html
function:
Every callback from :cowboy_rest
will take the same form. The function will receive request
and state
arguments to the function and it’ll respond with a tuple consisting of its reply and the request and state tuples.
If you start your project now with mix run --no-halt
and open localhost:8000
in a browser you should see the traditional “Hell, World” greeting.
But what if we wanted a json response?
Try curl -I --request GET --url http://localhost:8000/ --header 'accept: application/json'
in terminal.
You’ll get the following response:
So our resource does not support replying with application/json
, lets fix that.
The content_types_provided
callback is how we define what content-types our resource supports.
Our response here (aside from request and state which we’ll always have to return) is a list of tuples, each tuple containing a content-type and the name of the function that can generate the response in that content-type.
Lets add the to_json
function:
Restart cowboy
after making the changes (take a look at remix if you find it annoying restarting cowboy
after each change).
Run that curl
command again:
curl --request GET --url http://localhost:8000/ --header 'accept: application/json'
. Note that the -I
argument has been removed.
Sucess! We now have our JSON response. We can now also understand why earlier it was sufficient to just implement the to_html
function. cowboy_rest
defaults to serving html
by calling the to_html
function.
What happens if you don’t set an accept
header? And what happens if you change the order of the tuples in our reply from content_types_provided
?
Answer: If no accept
header is present in the request cowboy_rest
chooses to reply with the first content-type in the content_types_provided list.
The “Todo” handler
Lets extend the example with new resource for getting a list of todos and creating a todos.
First the dispatcher is extended and then the resource gets defined in its own module.
Extending the dispatcher
Add {"/todo", CowboyRestExample.TodoResource, []}
the list of routes in CowboyRestExample.Dispatch.routes
.
Next create the CowboyRestExample.TodoResource
module as below:
There is a new callback here: rest_init
. Its the first callback the resource gets after doing a protocol upgrade to :cowboy_rest
.
It allows us to set the initial state and return it as the last element of the response tuple, here its initialized as a empty map.
We only want to respond with json
so add a content_types_accepted
that makes that true:
Now we want to be able to get the list Todos. These will be fetched from a MySql database using ecto. I wont show the setup here but take a look at the github repo here if you want the details.
We now have a choice to make: What reply do we want if there are no todos? Empty list or a 404
status code?
Here we’ll use the 404
status code because it allows me to introduce the next callback: resource_exists
. Returning false
from this function will yield a 404
reply. It also makes for a nice place to actually fetch the Todos from the database.
In essence we’re pulling all the todos from the database and adding it to our state map under the :todos
key.
Now that we have a list of Todos in our state we can transform those into JSON in the to_json
function:
And with that we have our list Todo resource done.
Posting a Todo
But a Todo example isn’t worth its salt if you can’t add todos. In order to do that we need define two additional callbacks and add a function saving the new todo to the database.
If you try and send a POST
at /todo
right now you’ll get a 405 Method not allowed
response back. To fix that add the following:
We now supports POST
. Trying another POST
at /todo
will result in a 415 Unsupported Media Type
response because we haven’t told :cowboy_rest
that we want to accept application/json
.
The content_types_accepted
callback allows us to state what content-types we accept.
The :from_json
function also needs defining:
The first line of that function reads the body of the request. And here we stumble onto one of the quirks of cowboy
all functions on :cowboy_req
will return a request tuple which may or may not differ from the one passed to it. As the :cowboy
docs state:
Whenever Req is returned, you must use this returned value and ignore any previous you may have had. This value contains various state informations which are necessary for Cowboy to do some lazy evaluation or cache results where appropriate.
The the second line uses Poison to decode the request body into a map.
The third line inserts a new Todo into the database.
And finally the fourth line tells :cowboy_rest
that we succeeded.
If you try posting now you’ll get at 204 No Content
response. Huh? I was expecting a 201 Created
response.
Well it turns out you should set a location header in the response when sending a 201
status code.
Luckily that is easily fixed:
We need to know the host
and the id
so that we can construct a link for the location
header. Then we can extend our reply to {true, "#{host}/todo/#{todo.id}"}
. That is: “Yes, we could understand the body you sent us and you can find the result over there”.
Try posting again. What? 303 See other
? 303
is what you would send if your service prevented duplicates by pointing the client at the preexisting version of what they tried to create.
And sure enough our resource_exists
did return true
indicating that the resource did already exist.
Ok, time to fix that.
So if the request is a POST
or there are no Todos we’ll return false
otherwise return true
. You might be tempted to try and use pattern matching you match different resource_exists
implementations based on the request method. But unfortunately the request argument is a humongous tuple that does not lend itself easily to pattern matching.
Lets try that POST
again. Bingo! A nice and shiny 201 Created
response!
Getting a single Todo
One final resource: Getting a single Todo.
Again extend the dispatcher add {"/todo/:id", CowboyRestExample.TodoResource, []}
to the routes.
If you have done any projects in Phoenix
the :id
syntax should seem familiar. Its tells :cowboy
to bind any value after /todo/
to the id :id
so that we may fetch is in our resource.
Now create the CowboyRestExample.SingleTodoResource
module:
The formula should seem familiar now: Upgrade the protocol to :cowboy_rest
, fetch the todo in resource_exists
and transform it to JSON in to_json
. But aren’t things getting a bit repetitive and thus drawing focus away from the differences between the two Todo resources?
Lets define a module with some defaults:
If we now add use CowboyRestExample.Defaults
we now skip implementing the callbacks for the functions that won’t differ across resources thus drawing out the differences between our resources. We have also defined some safe defaults for the security related is_authorized
and forbidden
callbacks. In effect denying access to a resource unless we explicitly grant access.
defoverridable [forbidden: 2]
allows the different resources to override the forbidden
callback (which would not be possible if we did not add defoverridable
).
As a final touch we add a very simple behaviour: CowboyRestExample.DefaultBehaviour
which just defines our own to_json
callback. This is here to help our future selves. If we forget to implement a to_json
function in a resource that uses CowboyRestExample.Defaults
we’ll get a nice compiler warning reminding us to do so.
You can find the final code on github.
And that brings us to the end of the example.
Why not just use Phoenix?
Phoenix is a valid choice for doing REST service. At work we have our first two Phoenix service moving into production just about now). And we really, really like Phoenix. But experience of building our first two services has given us the chance to evaluate if Phoenix is right for us.
Is the plug model right for us?
They way we handle requests seems like an ill fit for the plug
model.
We have to do authentication, authorization and JSON schema validation of every request received to our APIs.
Authorization is the same across resources so thats not the problem, but authentication differs across resource and method (for instance one user might have access creating a give resource but not altering it). So we end up with constructs like this:
We have much the same situation with performing JSON schema validations.
We don’t use templates, channels nor i18n
We’re building APIs (that we then build web apps on top of) and as a consequence we’re not using templates, channels nor i18n. So there’s a lot of Phoenix we’re not using.
Batteries not included
cowboy_rest
is not without its drawbacks among which are:
- No logging: By default
cowboy
performs no logging, so you’ll have to figure out your own - Erlang stacktraces: You’ll have to learn to read Erlang stacktraces. Which takes some getting used to.
- Way less creature comforts: Phoenix does a lot to help the developer along. Automatic recompilation, great getting started guides, and excellently helpful error messages. Cowboy has none of that.
Performance?
Performance has not been a factor for looking beyond Phoenix. Phoenix is plenty fast.
But below you’ll find a graph comparing Posting a Todo, getting a single Todo, getting a list of 10 todos and getting the “Hello, World” resource across cowboy
, Webmachine
and Phoenix
.
Test were conducted using wrk
running with 300 connections.
Wrk, MySql and phoenix/cowboy/webmachine are all running on the same machine (a Mid 2015 MacBook Pro).
Don’t read to much into the results - in real life most likely everything but phoenix/cowboy/webmachine is going to dominate the response times.