Writing a Haskell API Server, Part 2
Making the Domain Work for You
Back in part 1 we spent a fair amount of time working to make sure that as much of the low-level data logic of our Haskell api server was kept in the database as possible. Things like auto-incrementing ids, created_at/updated_at dates, nulls, referential integrity, and uniqueness are best dealt with at the lowest possible layer. We’ve found that unless these concerns are dealt with there, they’ll need to be dealt with over and over again on all higher layers.
In part two, we’ll move on into the Haskell realm where we can build up true domain logic rather than simple data integrity logic. As often happens in software, we begin to see a layered structure emerge. Just like we encapsulated a lot of the low-level data integrity in the data layer, we likewise started to build up domain logic encapsulated in types. We found that designing types carefully at the outset yields lots of benefits down the road. If we can write types in such a way as to make certain bugs lead to compiler errors, then that’s awesome; it’s a huge win. That translates to a whole bunch of tests that we don’t even have to write (because if we did write such a test, it wouldn’t even compile). Yaron Minsky coined the term “make illegal states unrepresentable” for this tactic. Scott Wlaschin expounds wonderfully on this topic in “Domain modelling with the F# type system.” Ken Fox has also written a great post on how this sort of type-first programming meshes really well with TDD. All of these resources are excellent if you are interested in what we’re up to here.
We found a number of techniques to be really helpful for modeling our domain. A common thing that Jon and I would say to one another is that we were “front-loading the headache,” meaning that we had to put in some effort upfront — which turned out to be well worth it as time went on. This definitely felt odd for me at first, but I got used to it. These techniques fell into a number of patterns which I’ll now illustrate with some examples.
Newtype all the things!
The first surprising thing that we found is that almost nothing in an application is actually a string. Sure, many things are commonly represented as strings, but they’re not really strings. Most of the time when we use a string for some data, there’s actually more structure embedded in there. Email addresses are a perfect example. We can write them out and store them in strings, but they really have a lot of structure:
name@example.com
^ ^
| |
| ` server part
` local part
In fact, if something is significant to the business domain, then probably isn’t a vanilla string (or int, or float, etc.). A good test for this is if you have to inspect inside a string, then that’s a strong hint that it isn’t just a string. As a developer, it’s our job to find out what those constraints are that prevent something from really being a string.
To that end, in our app we wrapped most data in newtype wrappers. This tells the compiler to treat the wrapped data as if it were completely novel even if at runtime the data is stored precisely as a string would be.
One entity in the API server is a Contact
, this type represents a user’s contact information. Here’s what it looks like:
data ContactFields = ContactFields
{ con_name :: ContactName
, con_email :: ContactEmail
, con_address :: Maybe ContactAddress
, con_url :: Maybe ContactURL
, con_nickname :: Maybe Nickname
, con_twitter :: Maybe TwitterHandle
, con_linkedIn :: Maybe LinkedInHandle
} deriving (Eq, Show, Typeable)
You may notice that this datatype is for ContactFields
rather than just Contact
— that’s very significant and I’ll explain it in the Separate lifecycle phases with types section. For now, just note that each field is named something like ContactName
and etc. Each of these is a newtype, defined like this:
newtype ContactName = ContactName ST.Text
deriving (Byteable, Eq, Show,
FromJSON, ToJSON,
Convertible SqlValue, Parsable)
Here you can see that a ContactName
is actually just Text
, but it is wrapped in such a way that the compiler will catch any errors in using it. A ContactName
could’t be used where we were expecting a BusinessName
(and so on). There is surely some (syntactic) overhead to adding all these newtypes, but they save us from writing a lot of tests and they document the API well. We found the tradeoff to be worth it.
Border security
The next piece that we attacked was the issues around the “border.” This is any place where we either get or receive data from outside the application. This turns out to be a fair number of places even in a simple API server! Examples are:
- when we serialize or de-serialize data to the database
- when we convert to or from JSON
- when we read parameters from a web request
Any time that we do one of these actions, we must parse the data and/or deal with any errors in communication. If we read some data from a POST, we must make sure that it actually conforms to our idea of ContactFields
.
Haskell has some really nice libraries for doing this. In the case of JSON we can use Aeson to attempt to parse JSON into a datatype (this is as shown before, just pay attention to the FromJSON
and ToJSON
in the deriving clause):
newtype ContactName = ContactName ST.Text
deriving (Byteable, Eq, Show,
FromJSON, ToJSON,
Convertible SqlValue, Parsable)
Aeson can almost figure out how to automatically convert to and from our datatype, but we have to provide some help since we’re using newtypes pervasively:
instance FromJSON EmailAddress where
parseJSON (String text) = maybe mzero pure . emailAddress $ SE.encodeUtf8 text
parseJSON _ = mzero
instance ToJSON EmailAddress where
toJSON = String . SE.decodeUtf8 . toByteString
In a few places, we have to provide these instances to show Aeson how to convert from an EmailAddress
into a String
(and vice versa). This is where you can really see that, by using a newtype, Haskell really has no idea what this type is.
Once we’ve defined the proper conversions, reading ContactFields
from a POST looks like this:
instance FromJSON ContactFields where
parseJSON (Object v) =
ContactFields <$> v .: "name"
<*> v .: "email"
<*> v .:? "address"
<*> v .:? "url"
<*> v .:? "nickname"
<*> v .:? "twitter"
<*> v .:? "linkedin"
parseJSON _ = mzero
We extract each individual field from the incoming JSON and then build a ContactFields
with the result. The .:?
operator is for fields that are optional. If these are null, we’ll be left with Nothing
s in the corresponding field. This lines up exactly with our datatype where, for example, an address (or ContactAddress
) is optional. These are modeled in the type as Maybe
s.
So that is how we convert from a JSON value, but how do we convert ContactFields
to a JSON value? We can’t. We simply do not provide a ToJSON
instance for ContactFields
. This prevents us from ever inadvertently sending ContactFields
to the API client. This begins to blend with the next section, “Separate lifecycle phases with types.” When writing the application, we determined that there’s no valid business reason to send the client what is essentially an incomplete and unsaved Contact
.
How do we create and save a Contact
? That`s what the next section is about.
Separate lifecycle phases with types
We determined early on that there’s a big difference from an entity that has been saved in the database and one that hasn’t. Predictably, we enforce this distinction in the type system. You’ve already seen the datatype for ContactFields
:
data ContactFields = ContactFields
{ con_name :: ContactName
, …Omitting this part…
} deriving (Eq, Show, Typeable)
As you saw in part one of this series, we only create auto-incrementing IDs in the data layer. Note that a Contact
is just a ContactID
paired with the ContactFields
that you’ve already seen:
data Contact = Contact
{ con_id :: ContactID
, con_fields :: ContactFields
} deriving (Eq, Show, Typeable)
That is, we’re distinguishing between unsaved and saved Contact
s. This gives us a convenient lifecycle handle on contacts:
-
ContactFields
for a newContact
are POSTed to a controller action -
The application validates that this is a reasonable and allowed thing to do
-
The application stores the
ContactFields
in the database -
The database returns (via the
RETURNING
sql statement) a fully-formedContact
-
The application deserializes the new
Contact
. SinceContact
is an instance ofToJSON
, the application can return it to the client as a response to the CREATE API action.
In almost every one of those steps, constraints on the validity of our data (enforced through types) limited the reasonable steps in our controller action. We end up with a very small and obvious controller action. Here’s an example of editing a Contact
for a given User
.
edit :: User -> CardMeActionM s ()
edit user = do
cid <- getContactIdFor user
contact <- Contact cid <$> fromParams
saved <- reqQuery $ Contact.update contact
json saved
Reading from the </span>do onward the events are nicely sequential.
- We get the
ContactID
for the given user, since this is a protected route we get theUser
via anauthenticate
helper (this involves checking headers & etc.). - We use that
ContactID
and the supplied params, viafromParams
to build a newContact
.fromParams
is typed in such a way as to gather thePOST
parameters necessary to createContactFields
— this is a polymorphic return type in action again. - Next we run a query
reqQuery
that updates an existingContact
with the one that we just created. The newContact
is returned assaved
- Finally, we send the contact as a
JSON
response. Remember, we can only do this with a fully-formedContact
, the program wouldn’t have compiled if we had attempted to send back theContactFields
that we got fromfromParams
.
Error handling
Though it looks like we don’t do any error checking here, each step can possibly fail. Let’s take the line, contact fromParams
. Here we’re creating a new Contact
from the submitted parameters. This requires that we’ve described a way to go from params to a ContactFields
datatype:
instance FromParams ContactFields where
fromParams = ContactFields <$> reqParam "name"
<*> reqParam "email"
<*> optParam "address"
<*> optParam "url"
<*> optParam "nickname"
<*> optParam "twitter"
<*> optParam "linkedin"
This sort of code should be starting to look familiar by now. We’ve specified how to convert from individual params into ContactFields
data. The reqParam
and optParam
determine if the param is required or not:
optParam :: (Parsable a) => T.Text -> CardMeActionM s (Maybe a)
optParam key = do
val params
case val of
Just unparsed -> case parseParam unparsed of
Left _ -> raise $ MalformedParam key
Right x -> return $ Just x
_ -> return Nothing
reqParam :: (Parsable a) => T.Text -> CardMeActionM s a
reqParam key = do
val return x
_ -> raise $ MissingParam key
There’s a lot going on in these examples, but let me sketch it out for you a little. Required parameters (reqParam
) are enforced by reusing the code for optional param, but then raise
-ing if the param wasn’t provided. So that means all the heavy lifting is in optParam
. Let’s look at what `optParam` does.
First we look up the param among all the params, params
, by the provided key
. If the param is missing, we immediately return a Nothing
to the controller action via a return Nothing
, the param wasn’t provided and that’s okay because it was optional. If the val
is present, we now have an unparsed, possibly-invalid param. We then run parseParam
on the unparsed
param. Haskell is polymorphic in the return type of functions, so whatever type (the a
in the type signature) we’re expecting will be what the parameter is expected to parse as. If that parse fails, we have a present-but-malformed parameter and we raise
that to the controller action. Otherwise, we’ve got a present-and-valid parameter so we return it.
I mentioned raise
a second ago. Despite the name, this is actually just a normal function, albeit one that can abort the Controller action with more information. The amazing thing is that this is all accomplished without any special handling by Haskell. At any stage within our `CardMeActionM` monad, we can interrupt the flow with an early return (an exception). These errors are propagated back to the outer function where they are handled. In our routes we’ve set up a fall-back route that looks for errors:
fallback :: CardMeException -> CardMeActionM s ()
fallback err = case err of
MissingAuthToken -> status status401 >> text "authorization header required"
UnauthorizedUser -> status status401 >> text "unauthorized"
NoQueryResults -> status status404 >> text "file not found"
NoContactForUser -> status status404 >> text "user has no associated contact"
(MissingParam p) -> status status422 >> text (LT.concat ["missing parameter: ", p])
(MalformedParam p) -> status status422 >> text (LT.concat ["malformed parameter: ", p])
ServerError t -> status status500 >> text t
-- using it in the routes:
routes :: CardMeServerM s ()
routes = defaultHandler fallback >> do
post "/capture" $ User.authenticate Contact.capture
-- rest of the controller actions follow...
These functional exceptions allow us to separate out error-handling code while still keeping local control flow (as opposed to the non-local control flow of regular exceptions).
Summary
We talked a lot in this post about doing some type-first designing to encode domain logic in the very types that we use to represent those concepts. We also did a lot of testing, which you haven’t seen in this post. Tests revolved around questions like can we round-trip a Contact through the database? These revealed the bulk of our bugs. Combined with a few issues while deploying (e.g. deserializing port numbers to Haskell) we almost got the magical worked on the first try effect.
My big takeaway from this portion of the work was just how untrustworthy the outside world is. All the bugs that we ran into happened at the borders of the application. Any time we read data in, that was also where bugs crept in. Parse early and mercilessly but then rest easy after that. That being said, debugging was fairly effortless. When we noticed undesired behavior, we found the relevant border crossing that the infraction must be located at and fixed it without much fuss.
Thanks
- Bendyworks for professional development time & the room to try new stuff
- Jon for putting in all the time that actually makes a project like this work. He’s always been the one that fixes the half-baked stuff that I write. Also, it has been immensely rewarding to pair-program on Haskell professionally.
- And last, but certainly not least, Joe Nelson for his utterly indefatigable work on everything Haskell, all of which I depend on constantly: haskell-vim-now, heroku-buildpack-ghc, postgrest and more. Also I want to thank Joe for hours of deep conversations during BayHac2014 about what’s possible using databases, Haskell, and so much more. My understanding about how to design applications like this wouldn’t have been possible without his insights. His urging (even as a voice in the back of my head) has caused me to finish more projects than anything else. Thanks Joe!