Finances in an Employee-Owned Technology Co-operative

If you are not already familiar with nilenso, I apologize. This article may not make any sense to you. Requested as an opinion piece in reply to an internal email on salaries, it hinges on a lot of context.

You may find that context interesting.

The email which spawned this discussion was from a new employee, suggesting that we raise salaries. We discuss such organizational and infrastructural changes on occasion (not often) and an open conversation is welcome.

However, as I’m leaving nilenso in July, it felt worthwhile to capture what would otherwise be a one-time, long-winded email to nilenso’s existing staff and transform that into an article which could capture one founding member’s opinion of what nilenso is, for all future staff — and other fans of Employee-owned Co-operatives—who would care to read it.

On Salaries

The original email had a fairly simple 3-part structure.

First: How much can we pay? …without compromising on organizational goals like maintaining a rainy-day cash buffer and financing experimental new ventures?

Second, a declaration of our rates & salaries. Highlighted is the delta between the two. Our rates vary a lot from project to project, but for the sake of example, imagine our rates average to $100/hour. (100 is a nice round number.) At $100/hour, our rates were high for Indian software consultancies, but not astronomical. A napkin calculation roughs $100/hour to over $100,000/year in revenue. Historically, very senior people at nilenso made about half this revenue figure as salary.

[F]or people with more experience and established reputation, there is an incentive to break away and start their own business so that they can pocket the difference.

(All quotes are from the aforementioned email.)

Third and last, a spectrum of starting salaries. These were listed for college grads of reputable Indian universities, such as NIT and IIT. Paymasters near the top included Uber and Goldman Sachs. Near the bottom were Samsung and American Express.

Some of the people I know from college are very good programmers and I’ll have a very hard [time] convincing them to join Nilenso [sic], because the difference between our salaries and the industry “standard” is too large.


[M]ost of our clients can pay pretty high salaries. From their perspective it isn’t an unreasonable strategy to hire a bunch of folks from nilenso instead of negotiating our high rates

It is possible this argument doesn’t seem irrational in isolation, so let’s break it down part by part. From hereon out, please automatically prefix any declaration I make with “as of this writing.”

Part 3: A unicorn pissing contest

Let’s start from the end, since—tellingly—the actionable question was actually posed first.

There are a number of reasons our clients have not frequently hired our staff away. First, and most strictly, some contracts we sign have a two-way no-hire clause. In these cases, our clients cannot hire our employees and we cannot hire theirs. However, just because the contract does not forbid poaching does not make it a good idea. Our preferred engagement model is a long-term, cooperative and collaborative relationship; if either side starts yanking the other entity’s employees, that’s a great way to either poison the engagement.

In addition to all of this, however, there is sustainability at stake. As global consultancies go, nilenso is very small and very good. The author of the email is our 20th co-op member. Unless a client is very comfortable burning bridges, hiring two of our staff today trims our numbers by a non-trivial 10%. We don’t work with cannibal business owners.

That isn’t to say no one has ever left nilenso this way. If it happens, we simply hope that person takes their decision very seriously. Some of our alumni left to join past clients — and we wished them well.

People do not work at nilenso to get rich.

People work here because they believe in something. As we’ll see, “something” isn’t defined by some manifesto or mission statement. The “something” isn’t likely to be found elsewhere and it’s even less likely to be the same for two different people at nilenso. “Something” probably isn’t a single thing within the mind of a single person. “Something” is fractured and ever-changing. Each fractured shard is at any given time either a hallucination manifesting in action or a miscalculation fading to black.

As complex as The Somethings are, there has thus far been a few consistent threads. One? Technical excellence. A potential client from Vietnam recently asked “what is the ratio of fresh grads you hire to senior developers?” Though it may not appear so on the surface, the idea that “I’ll have a very hard [time] convincing [people I know from college] to join” stems from the very same misconception.

nilenso is not a leveraged consultancy.

The misconception is that nilenso desires to behave as other consultancies by “leveraging”** their weakest employees (read: fresh grads) and charging the highest possible rate by inflating them with as few “senior”** developers as possible. The misconception is not unfounded. Most consultancies operate this way. We do not.

** industry terminology, not mine

Convincing college grads to apply to nilenso is not a problem we’d like to solve. We have college grads begging us for internships and offering to work for free.

We do not want to hire college grads.

When our clients hire nilenso, we don’t want them satisfied. We want them elated. With one or two exceptions, our most junior developers always have multiple years of experience. They joined us because they couldn’t find their Something anywhere else in the industry. Not only has a college grad not, by definition, owned her own software in production — she hasn’t had an opportunity to go look for Something yet. Go searching first—and learn to build and deploy software while you’re at it. Maybe she’ll find her Something. If she doesn’t, she can join a company like ours.

We are not Uber or Goldman Sachs. We don’t treat our female colleagues like shit, we don’t have an “asshole culture”, we haven’t received a failing grade from the Better Business Bureau, nor do we have a list of “controversies and legal issues” as long as your arm.

Some of us have worked for companies like these and we have done everything in our power to prevent nilenso from becoming such a company. We don’t need revenue in the billions to behave this way but racing to compete salaries with such companies does us zero favours in that arena.

There is perhaps no better way for a college kid to learn the value of a company like nilenso than to experience a cesspool like Uber firsthand for a couple years. Not that I recommend the grads start their search there, mind you.

Part 2: Where is all this magical money coming from?

In “Part 3”, when I say that nilenso is “very small and very good” I mean that from the bottom of my heart. We are very good. We don’t fantasize about our clients paying us $100/hour — our current clients pay us this because we are worth it. Our first month with every new client is always evaluatory (we even did a 3-month evaluation period once). This is an opportunity for our clients to fire us if they find us too expensive and for us to fire them if we find the relationship isn’t productive.

Why are our developers worth well over $10,000 USD per month?

There is a watermark here. One can calculate the watermark in a number of ways. The first and most obvious is: How many people at nilenso have ever been paid $120,000/year or higher in any country? Two. So that probably puts the watermark too high. How many people at nilenso could be paid $120,000/year (roughly 77 lakh rupees) today — specifically in Bangalore? Three.

These numbers are clearly too low. The entire value of our company does not hinge on two or three people. But these are the only objective numbers we have on this axis. If we wanted to reach for a subjective number, we could say the watermark exists beneath anyone to whom our rates can be actively attributed. I would say these people number seven.

Seven of twenty active knowledge workers does not mean we “leverage” (ugh, that word) the other thirteen. Other folks are very close or arguably cross this watermark. But every team has a natural pecking order (a hierarchy drawn in a straight line, whether you draw it with a pen or with your mind) and wherever we assume this watermark to be, our value to our customers is more than the sum of the parts.

The flaw in the “wow, we make so much money!” argument is that no matter where you place this watermark, you must actively acknowledge from where this value is generated: above the watermark. If there is an argument to be made for altering the salary curve to reflect this, that alteration always implies an inflection point at the watermark. People above the watermark should be paid more. People below the watermark should be paid less. Your financial goals as an employee of such a company are dictated by your ability to cross the watermark and stay above it.

That doesn’t sound like a place I’d like to work.

I’m guessing the author of the original email feels the same way. Being our most junior staff member means starting at the very bottom of this curve and a search for the source of a consultancy’s value is always upward-facing. That’s a big hill to climb and it’s a lot more likely that crossing the watermark will involve nilenso growing from 20 to 40 rather than the most junior employee leapfrogging over 10 other people. We grow slowly on purpose — and it’s not to keep junior people at the bottom of the pay scale.

We would rather grow inside. We want our most junior staff to become more senior not by stacking themselves on top of new junior people but by perfecting their execution.

[F]or people with more experience and established reputation, there is an incentive to break away and start their own business so that they can pocket the difference.

No one at nilenso has ever done this. Why not?

Part 1: Where does all this money go?

Now we get to the juicy bits. The interesting question is not “how much can we pay?” at all. If someone wants to double her salary she can very easily do so today and nilenso is entirely unequipped to deal with that except to wish her the best with her new job.

People do not work at nilenso to get rich.

People work at nilenso to find… something. Something. What is that Something? To answer this, we must ask: What is nilenso? This is the interesting question.

Nilenso Software LLP is, today, a co-op; nilenso is self-hosted. It is written in a homoiconic language. It is self-referential. It is self-describing. The specification is the paper the specification is written on. And while none of these qualities are the Something we were all after, they are the qualities which equip us to do anything we want with the company. We are uninhibited in finding our Somethings if we never need to beg for VC money, never need to give up control, and never need to grow before we’re ready.

“Anything we want” can include bumping our salaries by 10% or 50%. It can include handing out bonuses. It can include democratically deciding we want to execute across-the-board pay cuts. But these are all pretty boring axes to manipulate the business. You don’t have to work at a co-op (and therefore run it) for very long to realize the the classic definitions of success are mind-numbingly boring… albeit always tempting.

Anything we want? Here is a far more interesting list:

  • helping parents raise children in the modern world
  • protect privacy as a fundamental right
  • fuel our office with solar panels
  • make the activity of building free software a work of pride, again
  • liberate the data sciences with truly open data
  • globally redefine what constitutes a “normal workplace”
  • what do you want to do? does it take money to do it? (hint: yes)

Every rupee we keep inside nilenso, invest as nilenso, or spend as nilenso pushes us further toward a collective activity, a collective vision. Every rupee we divide and distribute, 5 paise per co-op member, is a step away from a collective vision. The farthest we can go in this direction is to distribute everything: nilenso saves nothing, invests nothing, and builds nothing. Well before that stage, nilenso becomes an Umbrella Consultancy and we are nilenso in name only… another army of contractors with a brand attached and a few rupees saved every month to pay the water bill.

It is entirely valid for the collective vision of nilenso to be “we don’t want to have a collective vision” but once that is decided, there is no going back. If you want to become a software product company (as we attempted with Relay and previously attempted with Kulu), an electric rickshaw taxi service (a business model we have seriously considered), or a cafe (a business model we have jokingly considered)… you can’t. You get to go back to Square One, where we were when we incorporated nilenso in 2013. But you get to go back with no money and no way to compete on salaries but to rebuild a vision from scratch.

Good luck. It wasn’t easy the first time.


There is no easy way to think about the future regarding corporate or personal finances and arrive at novel conclusions. Follow Steve Jobs’ advice, drop enough acid, and you may. But you are still unlikely to execute based on these conclusions when you are sober on Monday. Such contemplations demand an entire suite of thought exercises. This is my favourite:

Flip the Five Whys exercise around and you have the And Then What? exercise.

Once you are rich, then what? Once you have solved the original problem, then what? Once you have solved an entire category of problems, then what? Once you have taught others to do it, then what?

And then what?

And then what?

Designing for Open Source Software

Mattermost → Relay

When you hear the words “open source” attached to a product, what image comes to mind? Do you imagine an office full of designers obsessing over every little pixel? Or do you imagine an army of alpha-nerds piecing together a slap-dash knockoff of some proprietary software?

We all believe open source software is badly designed because, historically, it was. (#NotAllOpenSourceSoftware)

Thirty years ago, in 1988, the very concept of a license or design language for software was far beyond the average person. A decade later, everything was still awful. Software barely worked. Your PC definitely had a virus on it. HTML had a <blink> tag. Installing Linux required weeks of patience and belief in oneself — the kind of conviction I would need to train for a marathon. If there is one canonical failure we can promote as the poster child of this dark era, it is OpenOffice. Or StarOffice or LibreOffice or Apache OpenOffice or OOo or AOO or whatever name you might know it by. When the very name of the product is this broken it’s unlikely to succeed as a brand.

Fast forward ten more years. As of 2008, Firefox was eating Microsoft’s lunch. Firefox was open source, fast, and standards-compliant. It was easy to like Firefox. But we didn’t like Firefox. We loved Firefox. Firefox had a brand before it was Firefox. A brand isn’t a snappy name and a cute logo. Your product is your brand and your product is dictated by its design.

Image from Lowter

In 2018, user expectations demand that design asymptotically approach perfection: your product better be open, compatible, clean, fast, and powerful. When nilenso first made the move from Slack to Mattermost, we kept all of this in mind. From our very first deployment of Mattermost it felt whole. We chose Mattermost because it was so very close to the design we wanted, we knew we could help bring it the rest of the way.

The beginning

The design problems that currently exist with Mattermost aren’t just visual but interaction and experience related. One of the first issues we took up was adding infinite scroll to the channel history. We couldn’t fathom why users should manually click to load every page of history. So we fixed it!

In the not-too-distant future we will add all the little nuanced details which transform Relay into software you absolutely love. At the moment, however, our aim is to solve the obvious problems our users have with the Mattermost/Relay experience. Like infinite scroll, these are the problems that left our early users shouting at us “ugh… how have you not fixed this yet?!”

Such problems stem from the pains caused by inconsistencies and visual discrepancies which hamper usability. It is surprising to many how quickly these seemingly small issues amount to death by a thousand papercuts. It is equally surprising how quickly software becomes a joy to use once you get the basics right.

We’ll start with the basics — icons and fonts — and then move on to the specifics of how we redesigned Mattermost as Relay, piece by piece.


When we moved from Slack to Mattermost the difference in icons used was stark. All icons used in Mattermost were inconsistent with respect to weights. They were standing out of the interface rather than blending in.

After further digging, we realised that Mattermost uses the free version of Font Awesome. Don’t get me wrong, I love the guys at Font Awesome; I even backed their kickstarter. But not all icons being available in the free weights (regular & solid) is quite a bummer.

We had three rules for selecting an icon set:

  • Icons must be open source
  • Icons need to be extensive
  • Icons need to be available in different styles (line and solid)
Allow me to interrupt with an aside. Technically, it is possible for us to build commercial artefacts into the Relay client by keeping them out of the source tree, even though Relay is AGPL3-licensed (as a derivative of the mattermost-webapp). Why, then, do we feel it is essential that the icons be open source? Relay lives at the intersection of the principles of Free Software and the license which governs it. Relay stands on the shoulders of giants — not only the Mattermost source code but Linux, GoLang, React, and hundreds of other projects. Here, between the spirit and the letter of Open Philosophies, exists a great deal of nuance. Cutting through this nuance is a root concept, however: “share alike.” If we take something from the free/open software community, what we return to that community should afford the next recipient the same freedoms we had. If we tweak Relay’s design until it is absolutely perfect but that involves changes which Mattermost cannot absorb upstream, we have failed them and every other project we have built ourselves upon. Thus, our icons must have an open license. — Steven Deobald

Thanks Steve. Keeping the above mentioned points in mind, there were three icon sets that made the final round of discussion:

We chose the Clarity icon set because of styles, consistency, and the icons being part of a design language system. Clarity is an open source design system created and maintained by the good folks at VMware. Since VMware has a dedicated team pushing continuous updates there is little fear the icon set will be abandoned or go stale.


Our font selection followed the same rules:

  • Fonts must be open source
  • Fonts should be available in multiple weights

…along with these additions:

  • Fonts with large x-height for easy readability
  • Fonts with thick Regular weight so fonts will be crisp

We stress so much on font weights because Relay is, first and foremost, a messaging tool. In Relay, you spend most of your time reading and we want this experience to be as enjoyable as possible. Relay also has complete Markdown support and weights help establish hierarchy both within the app and within the messages themselves.

We had a few contenders which satisfied these requirements. PT Sans, Lato, and Noto Sans all have an enjoyable reading experience but after a lot of trial and error we chose Fira Sans.

Fira Sans is an incredible font. It has a healthy variety of weights and the compressed nature of the font holds the messaging interface together while keeping it crisp.

Did you know that Fira Sans was built on top of Erik Spiekermann’s commercial font FF Meta? And that Spiekermann was commissioned by the Mozilla Foundation! Read the whole story here

How Mattermost became Relay

Implementing the design basics improved the existing experience. But to make those changes truly delightful we need to fine-tune every aspect of the app. We have started with three major sections:

  1. Left sidebar
  2. Channel header
  3. Main messaging interface
Present Mattermost interface

1. Left sidebar

The sidebar’s Team section allows users to switch between teams within organisations that they’re a part of. The current Mattermost implementation truncates the team name and adds a byline. The problem with this is that it does not add any value since hovering over the team opens a tooltip with the team name anyway. We removed the byline and the truncated text.

The sidebar’s Channel section felt mashed together. There is a clear hierarchy intended, which includes channel headers such as “Favorite Channels” and a distinction between active and inactive channels. We brought this hierarchy to the surface. Active channels have a heavy weight and inactive channels fade away with a lower opacity, despite the increased crispness of Fira Sans.

Grouped channels now have tighter spacing and groups themselves have larger spacing, allowing group titles to have a smaller font. All together, this makes the hierarchy obvious even at a glance.

2. Channel header

In the channel header we cleaned up the icons. Here are the issues we encountered with the icons:

  • Most icons weren’t of the same weight
  • Some icons were of a different size
  • Some icons had some positioning overrides to align them

The old icons for the channel header stood out, drawing attention away from the main chat area. We wanted them instead to blend into the design — unobtrusive but still obvious. To do this we removed the borders and switched the icon set to Clarity.

In addition to icons, we fixed the CSS for the header and right sidebar, getting rid of all funny margins and relative positioning. We also added consistent heights, paddings, and sizes for each of the icons. These fixes don’t just add polish to the UI; once merged, they will also ensure it is easier for anyone in the community, including us, to customise Mattermost.

3. Messaging interface

The messaging interface is where users spend the most time. The messaging interface is also where Mattermost has most of its design inconsistencies, which are most glaring in the basics: alignment, spacing between posts, font size, and other elements on the interface such as the reply box.

After we moved to Fira Sans we bumped up the font size, made sure the headings and names are bolded to establish hierarchy, and fixed alignment and spacing issues to deliver a crisper and more enjoyable reading experience.


Accessibility matters. We have light-sensitive coworkers and we’re sure you do, too. That’s why Relay comes with light and dark themes out of the box.

There are many Relay/Mattermost-compatible themes available already but we will pour all our energy into these two. They already make Relay beautiful and with every kaizen they will only get better.

Of course, we don’t restrict you to these two themes. Relay also comes with community themes and Relay’s entire colour scheme is fully customisable… not just the sidebar (cough Slack cough). We are working to make Relay 100% customisable through CSS so users have complete control over every aspect of the UI, if they want.

Putting it all together

By following a consistent design language for icons, font, and themes we ensure all our “pieces” are whole unto themselves. Putting them back together, the application itself becomes whole.

Mattermost has published a design challenge that encourages designers from all over the world to improve Mattermost (and, hence, Relay) readability and usability. Our redesign addresses this challenge.

Sign up, try the redesign, and tell us what you think!

Along with visual design changes, Relay also aims to significantly improve its user experience. Our public Trello board contains our plan for Relay’s future.


“Design” is the 8th most popular tag on Medium. “Open Source” is ranked 290. Source code licenses will never be sexy for the same reason your city’s water utility company will never be. The water supply is inherently a part of your city’s design. Similarly, your freedom as a user is designed into software you buy. Your ownership of a product is but one more limb in the tree of design.

Open source software needs good design because good design respects you, the user.
Illustration by Shrishti Ambani

Designing for Open Source Software was originally published in The Relay on Medium, where people are continuing the conversation by highlighting and responding to this story.

Original post by Varun Pai - check out Stories by Varun Pai on Medium

Writing a Simple REST Web Service in PureScript - Part 2

To recap, in the first part of this two-part tutorial, we built a simple JSON REST web service in PureScript to create, update, get, list and delete users, backed by a Postgres database. In this part we’ll work on the rest of the features. The requirements are:

  1. validation of API requests.
  2. reading the server and database configs from environment variables.
  3. logging HTTP requests and debugging info.

But first,


What happens if we hit a URL on our server which does not exist? Let’s fire up the server and test it:

$ http GET http://localhost:4000/v1/random
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 148
Content-Security-Policy: default-src 'self'
Content-Type: text/html; charset=utf-8
Date: Sat, 30 Sep 2017 08:23:20 GMT
X-Content-Type-Options: nosniff
X-Powered-By: Express

<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<pre>Cannot GET /v1/random</pre>

We get back a default HTML response with a 404 status from Express. Since we are writing a JSON API, we should return a JSON response in this case too. We add the following code in the src/SimpleService/Server.purs file to add a catch-all route and send a 404 status with a JSON error message:

allRoutePattern matches all routes because it uses a "/.*" regular expression. We place it as the last route to match all the otherwise unrouted requests. Let’s see what is the result:

$ http GET http://localhost:4000/v1/random
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 27
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 08:46:46 GMT
ETag: W/"1b-772e0u4nrE48ogbR0KmKfSvrHUE"
X-Powered-By: Express

    "error": "Route not found"

Now we get a nicely formatted JSON response.

Another scenario is when our application throws some uncaught error. To simulate this, we shut down our postgres database and hit the server for listing users:

$ http GET http://localhost:4000/v1/users
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Content-Length: 372
Content-Security-Policy: default-src 'self'
Content-Type: text/html; charset=utf-8
Date: Sat, 30 Sep 2017 08:53:40 GMT
X-Content-Type-Options: nosniff
X-Powered-By: Express

<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<pre>Error: connect ECONNREFUSED<br> &nbsp; &nbsp;at Object._errnoException (util.js:1026:11)<br> &nbsp; &nbsp;at _exceptionWithHostPort (util.js:1049:20)<br> &nbsp; &nbsp;at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1174:14)</pre>

We get another default HTML response from Express with a 500 status. Again, in this case we’d like to return a JSON response. We add the following code to the src/SimpleService/Server.purs file:

We add the useOnError handler which comes with purescript-express to return the error message as a JSON response. Back on the command-line:

$ http GET http://localhost:4000/v1/users
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Content-Length: 47
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 09:01:37 GMT
ETag: W/"2f-cJuIW6961YCpo9TWDSZ9VWHLGHE"
X-Powered-By: Express

    "error": "connect ECONNREFUSED"

It works! Bugs are fixed now. We proceed to add next features.


Let’s recall the code to update a user from the src/SimpleService/Handler.purs file:

As we can see, the actual request handling logic is obfuscated by the request validation logic for the user id and the user name patch parameters. We also notice that we are using three constructs for validation here: Maybe, Either and if-then-else. However, we can use just Either to subsume all these cases as it can “carry” a failure as well as a success case. Either also comes with a nice monad transformer ExceptT which provides the do syntax for failure propagation. So we choose ExceptT as the base construct for our validation framework and write functions to upgrade Maybe and if-then-else to it. We add the following code to the src/SimpleService/Validation.purs file:

We re-export except from the Control.Monad.Except module. We also add a withValidation function which runs an ExceptT based validation and either returns an error response with a 422 status in case of a failed validation or runs the given action with the valid value in case of a successful validation.

Using these functions, we now write updateUser in the src/SimpleService/Handler.purs file as:

The validation logic has been extracted out in separate functions now which are composed using Applicative. The validation steps are composed using the ExceptT monad. We are now free to express the core logic of the function clearly. We rewrite the src/SimpleService/Handler.purs file using the validations:

module SimpleService.Handler where

import Prelude

import Control.Monad.Aff.Class (liftAff)
import Control.Monad.Trans.Class (lift)
import Data.Bifunctor (lmap)
import Data.Either (Either)
import Data.Foldable (intercalate)
import Data.Foreign (ForeignError, renderForeignError)
import Data.Foreign.Class (encode)
import Data.Foreign.NullOrUndefined (unNullOrUndefined)
import Data.Int (fromString)
import Data.List.NonEmpty (toList)
import Data.List.Types (NonEmptyList)
import Data.Maybe (Maybe(..))
import Data.Tuple (Tuple(..))
import Database.PostgreSQL as PG
import Node.Express.Handler (Handler)
import Node.Express.Request (getBody, getRouteParam)
import Node.Express.Response (end, sendJson, setStatus)
import SimpleService.Persistence as P
import SimpleService.Validation as V
import SimpleService.Types

getUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
getUser pool = V.withValidation getUserId \userId ->
  liftAff (PG.withConnection pool $ flip P.findUser userId) >>= case _ of
    Nothing -> respond 404 { error: "User not found with id: " <> show userId }
    Just user -> respond 200 (encode user)

deleteUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
deleteUser pool = V.withValidation getUserId \userId -> do
  found <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
    P.findUser conn userId >>= case _ of
      Nothing -> pure false
      Just _  -> do
        P.deleteUser conn userId
        pure true
  if found
    then respondNoContent 204
    else respond 404 { error: "User not found with id: " <> show userId }

createUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
createUser pool = V.withValidation getUser \user@(User _) -> do
  liftAff (PG.withConnection pool $ flip P.insertUser user)
  respondNoContent 201
    getUser = lift getBody
      >>= V.except <<< renderForeignErrors
      >>= V.exceptCond "User ID must be positive" (\(User user) -> > 0)
      >>= V.exceptCond "User name must not be empty" (\(User user) -> /= "")

updateUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
updateUser pool = V.withValidation (Tuple <$> getUserId <*> getUserPatch)
                                   \(Tuple userId (UserPatch userPatch)) ->
    case unNullOrUndefined of
      Nothing -> respondNoContent 204
      Just uName -> V.withValidation (getUserName uName) \userName -> do
        savedUser <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
          P.findUser conn userId >>= case _ of
            Nothing -> pure Nothing
            Just (User user) -> do
              let user' = User (user { name = userName })
              P.updateUser conn user'
              pure $ Just user'
        case savedUser of
          Nothing -> respond 404 { error: "User not found with id: " <> show userId }
          Just user -> respond 200 (encode user)
    getUserPatch = lift getBody >>= V.except <<< renderForeignErrors
    getUserName = V.exceptCond "User name must not be empty" (_ /= "")

listUsers :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
listUsers pool = liftAff (PG.withConnection pool P.listUsers) >>= encode >>> respond 200

getUserId :: forall eff. V.Validation eff Int
getUserId = lift (getRouteParam "id")
  >>= V.exceptMaybe "User ID is required"
  >>= fromString >>> V.exceptMaybe "User ID must be an integer"
  >>= V.exceptCond "User ID must be positive" (_ > 0)

renderForeignErrors :: forall a. Either (NonEmptyList ForeignError) a -> Either String a
renderForeignErrors = lmap (toList >>> map renderForeignError >>> intercalate ", ")

respond :: forall eff a. Int -> a -> Handler eff
respond status body = do
  setStatus status
  sendJson body

respondNoContent :: forall eff. Int -> Handler eff
respondNoContent status = do
  setStatus status

The code is much cleaner now. Let’s try out a few test cases:

$ http POST http://localhost:4000/v1/users id:=3 name=roger
HTTP/1.1 201 Created
Connection: keep-alive
Content-Length: 0
Date: Sat, 30 Sep 2017 12:13:37 GMT
X-Powered-By: Express
$ http POST http://localhost:4000/v1/users id:=3
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 102
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:13:50 GMT
ETag: W/"66-/c4cfoquQZGwtDBUzHjJydJAHJ0"
X-Powered-By: Express

    "error": "Error at array index 0: (ErrorAtProperty \"name\" (TypeMismatch \"String\" \"Undefined\"))"
$ http POST http://localhost:4000/v1/users id:=3 name=""
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 39
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:02 GMT
ETag: W/"27-JQsh12xu/rEFdWy8REF4NMtBUB4"
X-Powered-By: Express

    "error": "User name must not be empty"
$ http POST http://localhost:4000/v1/users id:=0 name=roger
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 36
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:14 GMT
ETag: W/"24-Pvt1L4eGilBmVtaOGHlSReJ413E"
X-Powered-By: Express

    "error": "User ID must be positive"
$ http GET http://localhost:4000/v1/user/3
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 23
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:28 GMT
ETag: W/"17-1scpiB1FT9DBu9s4I1gNWSjH2go"
X-Powered-By: Express

    "id": 3,
    "name": "roger"
$ http GET http://localhost:4000/v1/user/asdf
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 38
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:40 GMT
ETag: W/"26-//tvORl1gGDUMwgSaqbEpJhuadI"
X-Powered-By: Express

    "error": "User ID must be an integer"
$ http GET http://localhost:4000/v1/user/-1
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 36
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:45 GMT
ETag: W/"24-Pvt1L4eGilBmVtaOGHlSReJ413E"
X-Powered-By: Express

    "error": "User ID must be positive"

It works as expected.


Right now our application configuration resides in the main function:

We are going to extract it out of the code and read it from the environment variables using the purescript-config package. First, we install the required packages using bower.

Now, we write the following code in the src/SimpleService/Config.purs file:

We use the applicative DSL provided in Data.Config module to build a description of our configuration. This description contains the keys and types of the configuration, for consumption by various interpreters. Then we use the fromEnv interpreter to read the config from the environment variables derived from the name fields in the records in the description in the readServerConfig function. We also write a bash script to set those environment variables in the development environment in the file:

Now we rewrite our src/Main.purs file to use the readServerConfig function:

If readServerConfig fails, we print the missing keys to the console and exit the process. Else we run the server with the read config.

To test this, we stop the server we ran in the beginning, source the config, and run it again:

It works! We test the failure case by opening another terminal which does not have the environment variables set:

Up next, we add logging to our application.


For logging, we use the purescript-logging package. We write a logger which logs to stdout; in the src/SimpleService/Logger.purs file:

module SimpleService.Logger
  ( debug
  , info
  , warn
  , error
  ) where

import Prelude

import Control.Logger as L
import Control.Monad.Eff.Class (class MonadEff, liftEff)
import Control.Monad.Eff.Console as C
import Control.Monad.Eff.Now (NOW, now)
import Data.DateTime.Instant (toDateTime)
import Data.Either (fromRight)
import Data.Formatter.DateTime (Formatter, format, parseFormatString)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)
import Data.String (toUpper)
import Partial.Unsafe (unsafePartial)

data Level = Debug | Info | Warn | Error

derive instance eqLevel :: Eq Level
derive instance ordLevel :: Ord Level
derive instance genericLevel :: Generic Level _

instance showLevel :: Show Level where
  show = toUpper <<< genericShow

type Entry =
  { level   :: Level
  , message :: String

dtFormatter :: Formatter
dtFormatter = unsafePartial $ fromRight $ parseFormatString "YYYY-MM-DD HH:mm:ss.SSS"

logger :: forall m e. (
          MonadEff (console :: C.CONSOLE, now :: NOW | e) m) => L.Logger m Entry
logger = L.Logger $ \{ level, message } -> liftEff do
  time <- toDateTime <$> now
  C.log $ "[" <> format dtFormatter time <> "] " <> show level <> " " <> message

log :: forall m e.
        MonadEff (console :: C.CONSOLE , now :: NOW | e) m
     => Entry -> m Unit
log entry@{level} = L.log (L.cfilter (\e -> e.level == level) logger) entry

debug :: forall m e.
         MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
debug message = log { level: Debug, message }

info :: forall m e.
        MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
info message = log { level: Info, message }

warn :: forall m e.
        MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
warn message = log { level: Warn, message }

error :: forall m e.
         MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
error message = log { level: Error, message }

purescript-logging lets us define our own logging levels and loggers. We define four log levels, and a log entry type with the log level and the message. Then we write the logger which will print the log entry to stdout along with the current time as a well formatted string. We define convenience functions for each log level.

Before we proceed, let’s install the required dependencies.

Now we add a request logger middleware to our server in the src/SimpleService/Server.purs file:

We also convert all our previous logging statements which used Console.log to use SimpleService.Logger and add logs in our handlers. We can see logging in effect by restarting the server and hitting it:


In this tutorial we learned how to create a simple JSON REST web service written in PureScript with persistence, validation, configuration and logging. The complete code for this tutorial can be found in github. This post can be discussed on r/purescript.

If you liked this post, please leave a comment.

Original post by Abhinav Sarkar - check out All posts on