Bootstrapping API project with Phoenix 1.3

SeriesPhoenix on Rails clock5 min read

Here's how to create a minimal Phoenix 1.3 setup for API server without HTML, Brunch, Gettext, channels and sessions.

Update: I've edited the article to include a clearer and more thorough explanation of reasoning behind removal of Gettext and a session plug from my lightweight API project.

Installing Phoenix release candidate

At the time of this write-up, Phoenix 1.3 is in RC0 release phase. So let's start with an explanation of how to install such non-stable release.

Installation of release candidate (or any arbitrary) version of Phoenix (such as the 1.3.0-rc.0 which this article is based upon) is only a tiny bit more complicated than what you may have come to expect from current stable path.

First, you may want to uninstall the existing stable release archive (if you previously installed it):

mix archive.uninstall phoenix_new.ez

Then you should visit the Phoenix archives page and download the version that you're interested in. Finally, as advertised by Phoenix installation guide, you can install the downloaded package:

mix archive.install ~/Downloads/phx_new-1.3.0-rc.0.ez

That's it. You should now be able to run mix phx.new targeting new version.

Creating new API project

You can invoke mix help phx.new to see the option list. In my case, I've run:

mix phx.new example-api --module ExampleAPI --app example_api --no-brunch --no-html

This gets us rid of phoenix_html and static Brunch setup. But we won't stop with that.

Extra luggage

Despite using phx.new option flags to the fullest, the newly created project still includes many parts that may not be needed for lightweight API project. Let's get rid of those now.

Removing Gettext

If you assume that the API messages (such as errors) are not supposed to be consumed by end users, then the initial Gettext setup, as convenient as it is, may be quite useless.

In my case, I've decided that my API will only return error codes (via the code property of JSON API error objects) and it'll be up to the front-end client (that lives in separate repo) to translate them for the user. This way, only the client repo will have to bother with translations and I won't have two separate Gettext translation sources to maintain.

First, delete the following files:

  • lib/example_api/web/gettext.ex
  • priv/gettext/errors.pot
  • priv/gettext/en/LC_MESSAGES/errors.po

Then remove the following code references:

  • gettext package from deps function in mix.exs
  • gettext compiler from compilers key in project function in mix.exs
  • translate_error function from ExampleAPI.Web.ErrorHelpers
  • all calls to import ExampleAPI.Web.Gettext in lib/example_api/web/web.ex

Finally, unlock the gettext dependency from mix.lock:

mix deps.unlock gettext

Removing channels and PubSub

I imagine that despite the increasing impact and undeniable convenience of WebSockets technology, many API servers still don't (yet) need anything but a classical HTTP flow. If that is your case, you may want to remove the channel boilerplate as well. You can always take a peak into your repo's Git history and restore them when the time comes for their great return.

PubSub is, to quote Phoenix documentation, a "nuts and bolts of organizing Channel communication". Since we don't need channels, we shouldn't need those nuts and bolts either.

First, delete the following files:

  • lib/example_api/web/channels/user_socket.ex
  • test/support/channel_case.ex

Then remove the following code references:

  • socket clause in lib/example_api/web/endpoint.ex
  • phoenix_pubsub package from deps function in mix.exs
  • pubsub key in ExampleAPI.Web.Endpoint config clause in config/config.exs

Unfortunately, it's not possible to unlock the phoenix_pubsub package entirely as it's a dependency of Phoenix itself, but at least the project code makes it clear about not being directly dependent on the package or channels functionality.

Removing unneeded plugs

As the final step, let's take a closer look at the plugs hooked by default into the endpoint (defined in lib/example_api/web/endpoint.ex). If you do decide that any of plugs found there may not be useful for you, just remove the corresponding plug clause and enjoy having the thinnest middleware stack possible.

The default RequestIdLoggerMethodOverride and Head plugs all seem to fit the typical API server use case. The Parsers plug also seems to be configured with reasonable and flexible defaults, allowing the API to consume params from URL, multipart body and JSON body.

This leaves us with the following strong candidates for removal.

Plug.Static

This plug serves static assets from priv/static when the server is running. Note that passing the --no-brunch option passed to mix phx.new got us rid of the Brunch setup, but Phoenix assumes we may still want to serve static files that we assemble with means other than Brunch. This may not be the case if the API is just an API and all the assets are bundled directly with the front-end project or otherwise out of scope.

Plug.Session

This one gives us an out-of-the-box per-client session store that lives on the client side in a cookie and that can be assumed to be secure and impossible to tamper with from the client side thanks to the server-side salt and optional encryption. While it's convenient for traditional full-stack web applications, you may want to rethink its use with API project for the following reasons:

  • APIs often live on different domains than their clients which may be problematic for a cookie-based session store, as cookies are designed as a per-domain resources
    Solution proposal: Cross-Origin Resource Sharing (CORS)
  • Cookies are also prone to CSRF attacks and the most common protection against those - the CSRF token - is tricky and unnatural to apply for APIs
    Solution proposal: LocalStorage, SessionStorage
  • Each web framework's session is implemented in a non-standard way, which makes it hard to share with other services if your API grows and spans multiple servers and technologies
    Solution proposal: JSON Web Tokens
  • Concept of session as "an ability to continue a conversation with specific user's browser through a series of HTTP calls" is obsolete in a world with web sockets
    Solution proposal: WebSockets (Phoenix channels)

If you stumble upon a use case where session seems to be necessary for your API, you may need to rethink your architecture and reconsider modern solutions to solve the above limitations. 

Summary

With the above setup, you get the most up-to-date Phoenix project, custom-tailored for a thin, blazing fast API server with just the components you really do need.

Although the mix phx.new task has some convenient customization options, sometimes you may want to go a step further and remove some more extra luggage. Fortunately, the structure of Phoenix projects is very explicit, boilerplate-free and straightforward (even more so in Phoenix 1.3 with the web directory removed) and it's not too hard to customize it.

Now, jump in and create your first API resource, perhaps by invoking mix phx.gen.json, because in Phoenix 1.3 this is where the real context-driven fun starts.