summaryrefslogtreecommitdiffstats
path: root/articles/cowboy2-qs/index.html
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2017-10-03 13:39:41 +0200
committerLoïc Hoguin <[email protected]>2017-10-03 13:39:41 +0200
commitb5d4cb91f80c833795a2d87050c3674bb7aecdc5 (patch)
tree62bf0ad8326006fcd3407fcb7c34c844c0dc0874 /articles/cowboy2-qs/index.html
parent1f8d51dd2692fc3978080419987bbe4d49a41a90 (diff)
downloadninenines.eu-b5d4cb91f80c833795a2d87050c3674bb7aecdc5.tar.gz
ninenines.eu-b5d4cb91f80c833795a2d87050c3674bb7aecdc5.tar.bz2
ninenines.eu-b5d4cb91f80c833795a2d87050c3674bb7aecdc5.zip
Update Hugo, docs
Diffstat (limited to 'articles/cowboy2-qs/index.html')
-rw-r--r--articles/cowboy2-qs/index.html402
1 files changed, 227 insertions, 175 deletions
diff --git a/articles/cowboy2-qs/index.html b/articles/cowboy2-qs/index.html
index 5d26a2ee..f8064d1e 100644
--- a/articles/cowboy2-qs/index.html
+++ b/articles/cowboy2-qs/index.html
@@ -7,7 +7,7 @@
<meta name="description" content="">
<meta name="author" content="Loïc Hoguin based on a design from (Soft10) Pol Cámara">
- <meta name="generator" content="Hugo 0.17" />
+ <meta name="generator" content="Hugo 0.26" />
<title>Nine Nines: Cowboy 2.0 and query strings</title>
@@ -74,155 +74,155 @@
</p>
</header>
-<div class="paragraph"><p>Now that Cowboy 1.0 is out, I can spend some of my time thinking
-about Cowboy 2.0 that will be released soon after Erlang/OTP 18.0.
-This entry discusses the proposed changes to query string handling
-in Cowboy.</p></div>
-<div class="paragraph"><p>Cowboy 2.0 will respond to user wishes by simplifying the interface
-of the <code>cowboy_req</code> module. Users want two things: less
-juggling with the Req variable, and more maps. Maps is the only
-dynamic key/value data structure in Erlang that we can match directly
-to extract values, allowing users to greatly simplify their code as
-they don&#8217;t need to call functions to do everything anymore.</p></div>
-<div class="paragraph"><p>Query strings are a good candidate for maps. It&#8217;s a list of
-key/values, so it&#8217;s pretty obvious we can win a lot by using maps.
-However query strings have one difference with maps: they can have
-duplicate keys.</p></div>
-<div class="paragraph"><p>How are we expected to handle duplicate keys? There&#8217;s no standard
-behavior. It&#8217;s up to applications. And looking at what is done in
-the wild, there&#8217;s no de facto standard either. While some ignore
-duplicate keys (keeping the first or the last they find), others
-require duplicate keys to end with <code>[]</code> to automatically
-put the values in a list, or even worse, languages like PHP even
-allow you to do things like <code>key[something][other]</code> and
-create a deep structure for it. Finally some allow any key to have
-duplicates and just gives you lists of key/values.</p></div>
-<div class="paragraph"><p>Cowboy so far had functions to retrieve query string values one
-value at a time, and if there were duplicates it would return the
-first it finds. It also has a function returning the entire list
-with all duplicates, allowing you to filter it to get all of them,
-and another function that returns the raw query string.</p></div>
-<div class="paragraph"><p>What are duplicates used for? Not that many things actually.</p></div>
-<div class="paragraph"><p>One use of duplicate keys is with HTML forms. It is common practice
-to give all related checkboxes the same name so you get a list of
-what&#8217;s been checked. When nothing is checked, nothing is sent at all,
-the key is not in the list.</p></div>
-<div class="paragraph"><p>Another use of duplicate keys is when generating forms. A good
-example of that would be a form that allows uploading any number
-of files. When you add a file, client-side code adds another field
-to the form. Repeat up to a certain limit.</p></div>
-<div class="paragraph"><p>And that&#8217;s about it. Of note is that HTML radio elements share
-the same name too, but only one key/value is sent, so they are not
-relevant here.</p></div>
-<div class="paragraph"><p>Normally this would be the part where I tell you how we solve
-this elegantly. But I had doubts. Why? Because there&#8217;s no good
-solutions to solving only this particular problem.</p></div>
-<div class="paragraph"><p>I then stopped thinking about duplicate keys for a minute and
-started to think about the larger problem.</p></div>
-<div class="paragraph"><p>Query strings are input data. They take a particular form,
-and may be sent as part of the URI or as part of the request
-body. We have other kinds of input data. We have headers and
-cookies and the request body in various forms. We also have
-path segments in URIs.</p></div>
-<div class="paragraph"><p>What do you do with input data? Well you use it to do
-something. But there is one thing that you almost always do
-(and if you don&#8217;t, you really should): you validate it and
-you map it into Erlang terms.</p></div>
-<div class="paragraph"><p>Cowboy left the user take care of validation and conversion
-into Erlang terms so far. Rather, it left the user take care
-of it everywhere except one place. Guess where? That&#8217;s right,
-bindings.</p></div>
-<div class="paragraph"><p>If you define routes with bindings then you have the option
-to provide constraints. Constraints can be used to do two things:
-validate the data and convert it in a more appropriate term. For
-example if you use the <code>int</code> constraint, Cowboy will
-make sure the binding is an integer, and will replace the value
-with the integer representation so that you can use it directly.
-In this particular case it not only routes the URI, but also
-validates and converts the bindings directly.</p></div>
-<div class="paragraph"><p>This is very relevant in the case of our duplicate keys,
-because if we have a list with duplicates of a key, chances
-are we want to convert that into a list of Erlang terms, and
-also make sure that all the elements in this list are expected.</p></div>
-<div class="paragraph"><p>The answer to this particular problem is simple. We need a
-function that will parse the query string and apply constraints.
-But this is not all, there is one other problem to be solved.</p></div>
-<div class="paragraph"><p>The other problem is that for the user some keys are mandatory
-and some are optional. Optional keys include the ones that
-correspond to HTML checkboxes: if the key for one or more
-checkbox is missing from the query string, we still want to
-have an empty list in our map so we can easily match. Matching
-maps is great, but not so much when values might be missing,
-so we have to normalize this data a little.</p></div>
-<div class="paragraph"><p>This problem is solved by allowing a default value. If the
-key is missing and a default exists, set it. If no default
-exists, then the key was mandatory and we want to crash.</p></div>
-<div class="paragraph"><p>I therefore make a proposal for changing the query string
-interface to three functions.</p></div>
-<div class="paragraph"><p>The first function already exists, it is <code>cowboy_req:qs(Req)</code>
-and it returns only the query string binary. No more Req returned.</p></div>
-<div class="paragraph"><p>The second function is a renaming of <code>cowboy_req:qs_vals(Req)</code>
-to something more explicit: <code>cowboy_req:parse_qs(Req)</code>.
-The new name implies that a parsing operation is done. It was implicit
-and cached before. It will be explicit and not cached anymore now.
-Again, no more Req returned.</p></div>
-<div class="paragraph"><p>The third function is the one I mentioned above. I think
-the interface <code>cowboy_req:match_qs(Req, Fields)</code> is
-most appropriate. It returns a normalized map that is the same
-regardless of optional fields being provided with the request,
-allowing for easy matching. It crashes if something went wrong.
-Still no Req returned.</p></div>
-<div class="paragraph"><p>I feel that this three function interface provides everything
-one would need to comfortably write applications. You can get
-low level and get the query string directly; you can get a list
-of key/value binaries without any additional processing and do it
-on your own; or you can get a processed map that contains Erlang
-terms ready to be used.</p></div>
-<div class="paragraph"><p>I strongly believe that by democratizing the constraints to
-more than just bindings, but also to query string, cookies and
-other key/values in Cowboy, we can allow the developer to quickly
-and easily go from HTTP request to Erlang function calls. The
-constraints are reusable functions that can serve as guards
-against unwanted data, providing convenience in the process.</p></div>
-<div class="paragraph"><p>Your handlers will not look like an endless series of calls
-to get and convert the input data, they will instead be just
-one call at the beginning followed by the actual application
-logic, thanks to constraints and maps.</p></div>
-<div class="listingblock">
-<div class="content"><!-- Generator: GNU source-highlight 3.1.8
-by Lorenzo Bettini
-http://www.lorenzobettini.it
-http://www.gnu.org/software/src-highlite -->
-<pre><tt><span style="font-weight: bold"><span style="color: #000000">handle</span></span>(<span style="color: #009900">Req</span>, <span style="color: #009900">State</span>) <span style="color: #990000">-&gt;</span>
- #{<span style="color: #FF6600">name</span><span style="color: #990000">:=</span><span style="color: #009900">Name</span>, <span style="color: #FF6600">email</span><span style="color: #990000">:=</span><span style="color: #009900">Email</span>, <span style="color: #FF6600">choices</span><span style="color: #990000">:=</span><span style="color: #009900">ChoicesList</span>, <span style="color: #FF6600">remember_me</span><span style="color: #990000">:=</span><span style="color: #009900">RememberMe</span>} <span style="color: #990000">=</span>
- <span style="font-weight: bold"><span style="color: #000000">cowboy_req:match_qs</span></span>(<span style="color: #009900">Req</span>, [
- <span style="font-weight: bold"><span style="color: #000080">name</span></span>, {<span style="color: #FF6600">email</span>, <span style="color: #FF6600">email</span>},
- {<span style="color: #FF6600">choices</span>, <span style="font-weight: bold"><span style="color: #0000FF">fun</span></span> <span style="font-weight: bold"><span style="color: #000000">check_choices</span></span><span style="color: #990000">/</span><span style="color: #993399">1</span>, []},
- {<span style="color: #FF6600">remember_me</span>, <span style="color: #FF6600">boolean</span>, <span style="color: #000080">false</span>}]),
- <span style="font-weight: bold"><span style="color: #000000">save_choices</span></span>(<span style="color: #009900">Name</span>, <span style="color: #009900">Email</span>, <span style="color: #009900">ChoicesList</span>),
- <span style="font-weight: bold"><span style="color: #0000FF">if</span></span> <span style="color: #009900">RememberMe</span> <span style="color: #990000">-&gt;</span> <span style="font-weight: bold"><span style="color: #000000">create_account</span></span>(<span style="color: #009900">Name</span>, <span style="color: #009900">Email</span>); <span style="color: #000080">true</span> <span style="color: #990000">-&gt;</span> <span style="color: #FF6600">ok</span> <span style="font-weight: bold"><span style="color: #0000FF">end</span></span>,
- {<span style="color: #FF6600">ok</span>, <span style="color: #009900">Req</span>, <span style="color: #009900">State</span>}<span style="color: #990000">.</span>
-
-<span style="font-weight: bold"><span style="color: #000000">check_choices</span></span>(<span style="color: #990000">&lt;&lt;</span><span style="color: #FF0000">"blue"</span><span style="color: #990000">&gt;&gt;</span>) <span style="color: #990000">-&gt;</span> {<span style="color: #000080">true</span>, <span style="color: #FF6600">blue</span>};
-<span style="font-weight: bold"><span style="color: #000000">check_choices</span></span>(<span style="color: #990000">&lt;&lt;</span><span style="color: #FF0000">"red"</span><span style="color: #990000">&gt;&gt;</span>) <span style="color: #990000">-&gt;</span> {<span style="color: #000080">true</span>, <span style="color: #FF6600">red</span>};
-<span style="font-weight: bold"><span style="color: #000000">check_choices</span></span>(<span style="color: #990000">_</span>) <span style="color: #990000">-&gt;</span> <span style="color: #000080">false</span>;</tt></pre></div></div>
-<div class="paragraph"><p>(Don&#8217;t look too closely at the structure yet.)</p></div>
-<div class="paragraph"><p>As you can see in the above snippet, it becomes really easy
-to go from query string to values. You can also use the map
-directly as it is guaranteed to only contain the keys you
-specified, any extra key is not returned.</p></div>
-<div class="paragraph"><p>This would I believe be a huge step up as we can now
-focus on writing applications instead of translating HTTP
-calls. Cowboy can now take care of it.</p></div>
-<div class="paragraph"><p>And to conclude, this also solves our duplicate keys
-dilemma, as they now automatically become a list of binaries,
-and this list is then checked against constraints that
-will fail if they were not expecting a list. And in the
-example above, it even converts the values to atoms for
-easier manipulation.</p></div>
-<div class="paragraph"><p>As usual, feedback is more than welcome, and I apologize
-for the rocky structure of this post as it contains all the
-thoughts that went into this rather than just the conclusion.</p></div>
+<div class="paragraph"><p>Now that Cowboy 1.0 is out, I can spend some of my time thinking
+about Cowboy 2.0 that will be released soon after Erlang/OTP 18.0.
+This entry discusses the proposed changes to query string handling
+in Cowboy.</p></div>
+<div class="paragraph"><p>Cowboy 2.0 will respond to user wishes by simplifying the interface
+of the <code>cowboy_req</code> module. Users want two things: less
+juggling with the Req variable, and more maps. Maps is the only
+dynamic key/value data structure in Erlang that we can match directly
+to extract values, allowing users to greatly simplify their code as
+they don&#8217;t need to call functions to do everything anymore.</p></div>
+<div class="paragraph"><p>Query strings are a good candidate for maps. It&#8217;s a list of
+key/values, so it&#8217;s pretty obvious we can win a lot by using maps.
+However query strings have one difference with maps: they can have
+duplicate keys.</p></div>
+<div class="paragraph"><p>How are we expected to handle duplicate keys? There&#8217;s no standard
+behavior. It&#8217;s up to applications. And looking at what is done in
+the wild, there&#8217;s no de facto standard either. While some ignore
+duplicate keys (keeping the first or the last they find), others
+require duplicate keys to end with <code>[]</code> to automatically
+put the values in a list, or even worse, languages like PHP even
+allow you to do things like <code>key[something][other]</code> and
+create a deep structure for it. Finally some allow any key to have
+duplicates and just gives you lists of key/values.</p></div>
+<div class="paragraph"><p>Cowboy so far had functions to retrieve query string values one
+value at a time, and if there were duplicates it would return the
+first it finds. It also has a function returning the entire list
+with all duplicates, allowing you to filter it to get all of them,
+and another function that returns the raw query string.</p></div>
+<div class="paragraph"><p>What are duplicates used for? Not that many things actually.</p></div>
+<div class="paragraph"><p>One use of duplicate keys is with HTML forms. It is common practice
+to give all related checkboxes the same name so you get a list of
+what&#8217;s been checked. When nothing is checked, nothing is sent at all,
+the key is not in the list.</p></div>
+<div class="paragraph"><p>Another use of duplicate keys is when generating forms. A good
+example of that would be a form that allows uploading any number
+of files. When you add a file, client-side code adds another field
+to the form. Repeat up to a certain limit.</p></div>
+<div class="paragraph"><p>And that&#8217;s about it. Of note is that HTML radio elements share
+the same name too, but only one key/value is sent, so they are not
+relevant here.</p></div>
+<div class="paragraph"><p>Normally this would be the part where I tell you how we solve
+this elegantly. But I had doubts. Why? Because there&#8217;s no good
+solutions to solving only this particular problem.</p></div>
+<div class="paragraph"><p>I then stopped thinking about duplicate keys for a minute and
+started to think about the larger problem.</p></div>
+<div class="paragraph"><p>Query strings are input data. They take a particular form,
+and may be sent as part of the URI or as part of the request
+body. We have other kinds of input data. We have headers and
+cookies and the request body in various forms. We also have
+path segments in URIs.</p></div>
+<div class="paragraph"><p>What do you do with input data? Well you use it to do
+something. But there is one thing that you almost always do
+(and if you don&#8217;t, you really should): you validate it and
+you map it into Erlang terms.</p></div>
+<div class="paragraph"><p>Cowboy left the user take care of validation and conversion
+into Erlang terms so far. Rather, it left the user take care
+of it everywhere except one place. Guess where? That&#8217;s right,
+bindings.</p></div>
+<div class="paragraph"><p>If you define routes with bindings then you have the option
+to provide constraints. Constraints can be used to do two things:
+validate the data and convert it in a more appropriate term. For
+example if you use the <code>int</code> constraint, Cowboy will
+make sure the binding is an integer, and will replace the value
+with the integer representation so that you can use it directly.
+In this particular case it not only routes the URI, but also
+validates and converts the bindings directly.</p></div>
+<div class="paragraph"><p>This is very relevant in the case of our duplicate keys,
+because if we have a list with duplicates of a key, chances
+are we want to convert that into a list of Erlang terms, and
+also make sure that all the elements in this list are expected.</p></div>
+<div class="paragraph"><p>The answer to this particular problem is simple. We need a
+function that will parse the query string and apply constraints.
+But this is not all, there is one other problem to be solved.</p></div>
+<div class="paragraph"><p>The other problem is that for the user some keys are mandatory
+and some are optional. Optional keys include the ones that
+correspond to HTML checkboxes: if the key for one or more
+checkbox is missing from the query string, we still want to
+have an empty list in our map so we can easily match. Matching
+maps is great, but not so much when values might be missing,
+so we have to normalize this data a little.</p></div>
+<div class="paragraph"><p>This problem is solved by allowing a default value. If the
+key is missing and a default exists, set it. If no default
+exists, then the key was mandatory and we want to crash.</p></div>
+<div class="paragraph"><p>I therefore make a proposal for changing the query string
+interface to three functions.</p></div>
+<div class="paragraph"><p>The first function already exists, it is <code>cowboy_req:qs(Req)</code>
+and it returns only the query string binary. No more Req returned.</p></div>
+<div class="paragraph"><p>The second function is a renaming of <code>cowboy_req:qs_vals(Req)</code>
+to something more explicit: <code>cowboy_req:parse_qs(Req)</code>.
+The new name implies that a parsing operation is done. It was implicit
+and cached before. It will be explicit and not cached anymore now.
+Again, no more Req returned.</p></div>
+<div class="paragraph"><p>The third function is the one I mentioned above. I think
+the interface <code>cowboy_req:match_qs(Req, Fields)</code> is
+most appropriate. It returns a normalized map that is the same
+regardless of optional fields being provided with the request,
+allowing for easy matching. It crashes if something went wrong.
+Still no Req returned.</p></div>
+<div class="paragraph"><p>I feel that this three function interface provides everything
+one would need to comfortably write applications. You can get
+low level and get the query string directly; you can get a list
+of key/value binaries without any additional processing and do it
+on your own; or you can get a processed map that contains Erlang
+terms ready to be used.</p></div>
+<div class="paragraph"><p>I strongly believe that by democratizing the constraints to
+more than just bindings, but also to query string, cookies and
+other key/values in Cowboy, we can allow the developer to quickly
+and easily go from HTTP request to Erlang function calls. The
+constraints are reusable functions that can serve as guards
+against unwanted data, providing convenience in the process.</p></div>
+<div class="paragraph"><p>Your handlers will not look like an endless series of calls
+to get and convert the input data, they will instead be just
+one call at the beginning followed by the actual application
+logic, thanks to constraints and maps.</p></div>
+<div class="listingblock">
+<div class="content"><!-- Generator: GNU source-highlight 3.1.8
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre><tt><span style="font-weight: bold"><span style="color: #000000">handle</span></span>(<span style="color: #009900">Req</span>, <span style="color: #009900">State</span>) <span style="color: #990000">-&gt;</span>
+ #{<span style="color: #FF6600">name</span><span style="color: #990000">:=</span><span style="color: #009900">Name</span>, <span style="color: #FF6600">email</span><span style="color: #990000">:=</span><span style="color: #009900">Email</span>, <span style="color: #FF6600">choices</span><span style="color: #990000">:=</span><span style="color: #009900">ChoicesList</span>, <span style="color: #FF6600">remember_me</span><span style="color: #990000">:=</span><span style="color: #009900">RememberMe</span>} <span style="color: #990000">=</span>
+ <span style="font-weight: bold"><span style="color: #000000">cowboy_req:match_qs</span></span>(<span style="color: #009900">Req</span>, [
+ <span style="font-weight: bold"><span style="color: #000080">name</span></span>, {<span style="color: #FF6600">email</span>, <span style="color: #FF6600">email</span>},
+ {<span style="color: #FF6600">choices</span>, <span style="font-weight: bold"><span style="color: #0000FF">fun</span></span> <span style="font-weight: bold"><span style="color: #000000">check_choices</span></span><span style="color: #990000">/</span><span style="color: #993399">1</span>, []},
+ {<span style="color: #FF6600">remember_me</span>, <span style="color: #FF6600">boolean</span>, <span style="color: #000080">false</span>}]),
+ <span style="font-weight: bold"><span style="color: #000000">save_choices</span></span>(<span style="color: #009900">Name</span>, <span style="color: #009900">Email</span>, <span style="color: #009900">ChoicesList</span>),
+ <span style="font-weight: bold"><span style="color: #0000FF">if</span></span> <span style="color: #009900">RememberMe</span> <span style="color: #990000">-&gt;</span> <span style="font-weight: bold"><span style="color: #000000">create_account</span></span>(<span style="color: #009900">Name</span>, <span style="color: #009900">Email</span>); <span style="color: #000080">true</span> <span style="color: #990000">-&gt;</span> <span style="color: #FF6600">ok</span> <span style="font-weight: bold"><span style="color: #0000FF">end</span></span>,
+ {<span style="color: #FF6600">ok</span>, <span style="color: #009900">Req</span>, <span style="color: #009900">State</span>}<span style="color: #990000">.</span>
+
+<span style="font-weight: bold"><span style="color: #000000">check_choices</span></span>(<span style="color: #990000">&lt;&lt;</span><span style="color: #FF0000">"blue"</span><span style="color: #990000">&gt;&gt;</span>) <span style="color: #990000">-&gt;</span> {<span style="color: #000080">true</span>, <span style="color: #FF6600">blue</span>};
+<span style="font-weight: bold"><span style="color: #000000">check_choices</span></span>(<span style="color: #990000">&lt;&lt;</span><span style="color: #FF0000">"red"</span><span style="color: #990000">&gt;&gt;</span>) <span style="color: #990000">-&gt;</span> {<span style="color: #000080">true</span>, <span style="color: #FF6600">red</span>};
+<span style="font-weight: bold"><span style="color: #000000">check_choices</span></span>(<span style="color: #990000">_</span>) <span style="color: #990000">-&gt;</span> <span style="color: #000080">false</span>;</tt></pre></div></div>
+<div class="paragraph"><p>(Don&#8217;t look too closely at the structure yet.)</p></div>
+<div class="paragraph"><p>As you can see in the above snippet, it becomes really easy
+to go from query string to values. You can also use the map
+directly as it is guaranteed to only contain the keys you
+specified, any extra key is not returned.</p></div>
+<div class="paragraph"><p>This would I believe be a huge step up as we can now
+focus on writing applications instead of translating HTTP
+calls. Cowboy can now take care of it.</p></div>
+<div class="paragraph"><p>And to conclude, this also solves our duplicate keys
+dilemma, as they now automatically become a list of binaries,
+and this list is then checked against constraints that
+will fail if they were not expecting a list. And in the
+example above, it even converts the values to atoms for
+easier manipulation.</p></div>
+<div class="paragraph"><p>As usual, feedback is more than welcome, and I apologize
+for the rocky structure of this post as it contains all the
+thoughts that went into this rather than just the conclusion.</p></div>
</article>
</div>
@@ -231,55 +231,107 @@ thoughts that went into this rather than just the conclusion.</p></div>
<h3>More articles</h3>
<ul id="articles-nav" class="extra_margin">
- <li><a href="https://ninenines.eu/articles/cowboy-2.0.0-rc.2/">Cowboy 2.0 release candidate 2</a></li>
+
+ <li><a href="https://ninenines.eu/articles/cowboy-2.0.0-rc.2/">Cowboy 2.0 release candidate 2</a></li>
+
+
+
+ <li><a href="https://ninenines.eu/articles/cowboy-2.0.0-rc.1/">Cowboy 2.0 release candidate 1</a></li>
+
- <li><a href="https://ninenines.eu/articles/cowboy-2.0.0-rc.1/">Cowboy 2.0 release candidate 1</a></li>
+
+ <li><a href="https://ninenines.eu/articles/the-elephant-in-the-room/">The elephant in the room</a></li>
+
- <li><a href="https://ninenines.eu/articles/the-elephant-in-the-room/">The elephant in the room</a></li>
+
+ <li><a href="https://ninenines.eu/articles/dont-let-it-crash/">Don&#39;t let it crash</a></li>
+
- <li><a href="https://ninenines.eu/articles/dont-let-it-crash/">Don&#39;t let it crash</a></li>
+
+ <li><a href="https://ninenines.eu/articles/cowboy-2.0.0-pre.4/">Cowboy 2.0 pre-release 4</a></li>
+
- <li><a href="https://ninenines.eu/articles/cowboy-2.0.0-pre.4/">Cowboy 2.0 pre-release 4</a></li>
+
+ <li><a href="https://ninenines.eu/articles/ranch-1.3/">Ranch 1.3</a></li>
+
- <li><a href="https://ninenines.eu/articles/ranch-1.3/">Ranch 1.3</a></li>
+
+ <li><a href="https://ninenines.eu/articles/ml-archives/">Mailing list archived</a></li>
+
- <li><a href="https://ninenines.eu/articles/ml-archives/">Mailing list archived</a></li>
+
+ <li><a href="https://ninenines.eu/articles/website-update/">Website update</a></li>
+
- <li><a href="https://ninenines.eu/articles/website-update/">Website update</a></li>
+
+ <li><a href="https://ninenines.eu/articles/erlanger-playbook-september-2015-update/">The Erlanger Playbook September 2015 Update</a></li>
+
- <li><a href="https://ninenines.eu/articles/erlanger-playbook-september-2015-update/">The Erlanger Playbook September 2015 Update</a></li>
+
+ <li><a href="https://ninenines.eu/articles/erlanger-playbook/">The Erlanger Playbook</a></li>
+
- <li><a href="https://ninenines.eu/articles/erlanger-playbook/">The Erlanger Playbook</a></li>
+
+ <li><a href="https://ninenines.eu/articles/erlang-validate-utf8/">Validating UTF-8 binaries with Erlang</a></li>
+
- <li><a href="https://ninenines.eu/articles/erlang-validate-utf8/">Validating UTF-8 binaries with Erlang</a></li>
+
+ <li><a href="https://ninenines.eu/articles/on-open-source/">On open source</a></li>
+
- <li><a href="https://ninenines.eu/articles/on-open-source/">On open source</a></li>
+
+ <li><a href="https://ninenines.eu/articles/the-story-so-far/">The story so far</a></li>
+
- <li><a href="https://ninenines.eu/articles/the-story-so-far/">The story so far</a></li>
+
+ <li><a href="https://ninenines.eu/articles/cowboy2-qs/">Cowboy 2.0 and query strings</a></li>
+
- <li><a href="https://ninenines.eu/articles/cowboy2-qs/">Cowboy 2.0 and query strings</a></li>
+
+ <li><a href="https://ninenines.eu/articles/january-2014-status/">January 2014 status</a></li>
+
- <li><a href="https://ninenines.eu/articles/january-2014-status/">January 2014 status</a></li>
+
+ <li><a href="https://ninenines.eu/articles/farwest-funded/">Farwest got funded!</a></li>
+
- <li><a href="https://ninenines.eu/articles/farwest-funded/">Farwest got funded!</a></li>
+
+ <li><a href="https://ninenines.eu/articles/erlang.mk-and-relx/">Build Erlang releases with Erlang.mk and Relx</a></li>
+
- <li><a href="https://ninenines.eu/articles/erlang.mk-and-relx/">Build Erlang releases with Erlang.mk and Relx</a></li>
+
+ <li><a href="https://ninenines.eu/articles/xerl-0.5-intermediate-module/">Xerl: intermediate module</a></li>
+
- <li><a href="https://ninenines.eu/articles/xerl-0.5-intermediate-module/">Xerl: intermediate module</a></li>
+
+ <li><a href="https://ninenines.eu/articles/xerl-0.4-expression-separator/">Xerl: expression separator</a></li>
+
- <li><a href="https://ninenines.eu/articles/xerl-0.4-expression-separator/">Xerl: expression separator</a></li>
+
+ <li><a href="https://ninenines.eu/articles/erlang-scalability/">Erlang Scalability</a></li>
+
- <li><a href="https://ninenines.eu/articles/erlang-scalability/">Erlang Scalability</a></li>
+
+ <li><a href="https://ninenines.eu/articles/xerl-0.3-atomic-expressions/">Xerl: atomic expressions</a></li>
+
- <li><a href="https://ninenines.eu/articles/xerl-0.3-atomic-expressions/">Xerl: atomic expressions</a></li>
+
+ <li><a href="https://ninenines.eu/articles/xerl-0.2-two-modules/">Xerl: two modules</a></li>
+
- <li><a href="https://ninenines.eu/articles/xerl-0.2-two-modules/">Xerl: two modules</a></li>
+
+ <li><a href="https://ninenines.eu/articles/xerl-0.1-empty-modules/">Xerl: empty modules</a></li>
+
- <li><a href="https://ninenines.eu/articles/xerl-0.1-empty-modules/">Xerl: empty modules</a></li>
+
+ <li><a href="https://ninenines.eu/articles/ranch-ftp/">Build an FTP Server with Ranch in 30 Minutes</a></li>
+
- <li><a href="https://ninenines.eu/articles/ranch-ftp/">Build an FTP Server with Ranch in 30 Minutes</a></li>
+
+ <li><a href="https://ninenines.eu/articles/tictactoe/">Erlang Tic Tac Toe</a></li>
+
- <li><a href="https://ninenines.eu/articles/tictactoe/">Erlang Tic Tac Toe</a></li>
+
</ul>