In part 2 of Building a Blog I will talk about how the small HTTP API used by this blog was implemented. Once again, because I like Haskell, I’ll use the Scotty web framework to listen to HTTP requests and run SQLite queries with the Beam library.
You don’t really need to write a backend for a blog, but I wanted to do it just for fun. Haskell has a great runtime for web apps so I thought it would be cool to try it out in a “real world” application. Besides, I wanted a way to collect data about the site’s usage without using third party JavaScript. Previously I talked about how privacy matters and that this sort of thing should be opt-in. To gather this data I decided to add a like button, which users can voluntarily click to send some anonymous data to my server so that I can count how many people liked my posts. To achieve this, I need some endpoints that can update and read the likes counter with POST and GET methods respectively. The Scotty framework is simple enough to let us do this with just a few lines of code.
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Web.Scotty (get, post, scotty)
import Web.Actions (getLikes, postLike)
main :: IO ()
= scotty 8008 $ do
main "/likes/:postId" getLikes
get "/likes/:postId" postLike post
This web app listens for HTTP requests on port 8008 and exposes two endpoints
GET /likes
and POST /like
that are handled by the functions getLikes
and
postLike
respectively. They both take the post’s ID as a URL parameter. The
basic idea is that when a POST /like
request is received, the likes counter
for that post is incremented by one unit in the database. On the other hand,
when a GET /likes
request is received the number of likes for that post
is queried in the database. This sounds fairly easy right? SQLite is my favorite
database, and I’m pretty sure that it is good enough for this kind of
website, so I just went with that. All
that was missing was a way to talk to the database from Haskell.
Managing SQLite with Beam
I’ve always disliked ORMs and any other attempts to abstract SQL from application code. Yes, I understand that SQL injection is a thing, but I like SQL and, although it is a standard, every database that implements it is different. ORMs can be very problematic. You might end up with a simple system if you just embrace SQL, even if it doesn’t fit with the way your programming language does things.
I know that it’s probably not the most rational thing to do given my stance on
database abstractions but I decided to give Beam a try. The reason is that I don’t claim to
be right about absolutely everything and I was very curious about what would
be the “Haskell way” of working with databases. One cool thing about Beam,
and other similar Haskell libraries, is that it makes the user responsible for
opening and managing the database instead of magically doing it for you under
the hood. This is actually good because it means that I can always fallback to
sending raw queries with sqlite-simple
if I don’t like the queries
generated by Beam, so there is nothing to worry about.
Learning Beam wasn’t easy for me. It uses lots of language extensions, and needs a lot of boilerplate that I can’t really understand. Nevertheless, I am happy with the result. I tried reading a few tutorials, but I always ended up reading the official docs because the things that I needed to do weren’t as simple as the ones that tutorials usually do, plus Beam’s documentation site is pretty neat.
The first thing that I had to do was to create the database schema. I wanted to be in full control of it so I generated it with SQL instead of letting Beam do it. I ended up with this:
create table posts(
id integer primary key,
string_id text
);
create table readers(
id integer primary key,
ip_address text,
user_agent text
);
create table likes(
id integer primary key,
integer,
reader_key__id integer,
post_key__id foreign key(reader_key__id)
references readers(id)
on delete cascade,
foreign key(post_key__id)
references posts(id)
on delete cascade
);
I only have three tables. The first one is for posts, and excluding the primary key, it only has one field: the ID of the post, which is just a string with the last segment in its path.
The second table is for readers. It stores IP addresses and user agent strings.
The reader table also has an index that ensure that each ip_address
and
user_agent
pair must be unique, which means that for this system a reader is
a unique combination of IP address and user agent. This is totally not real, but
it is a good approximation for me.
The third table, “likes,” is just a relation between many readers and many posts.
I let users like the same post as many times as they want because the
ip_address
and user_agent
combination of the “readers” table is not unique
at all. A like is merely a combination of a user and a post, somebody liked
something. The “readers” table’s only purpose is to provide a little more data
about the person who liked the post.
Then I needed to implement this exact same schema with Beam. This has a lot of boilerplate, and I’m definitely not the best to explain it, but you can see it here:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Database.Schema (
BlogDB(_blogLikes, _blogPosts, _blogReaders),
Like,
LikeT(Like, _likeReaderKey, _likePostKey),
Post,
PostT(Post, _postStringId),
Reader,
ReaderT(Reader, _readerId, _readerIpAddress,
_readerUserAgent),where
blogDb)
import qualified Data.Text.Lazy as Text
import Database.Beam
-- specify types for the tables
data PostT f = Post
_postId :: Columnar f Int
{ _postStringId :: Columnar f Text.Text
,deriving (Generic)
}
data ReaderT f = Reader
_readerId :: Columnar f Int
{ _readerIpAddress :: Columnar f Text.Text
, _readerUserAgent :: Columnar f Text.Text
,deriving (Generic)
}
data LikeT f = Like
_likeId :: Columnar f Int
{ _likeReaderKey :: PrimaryKey ReaderT f
, _likePostKey :: PrimaryKey PostT f
,deriving (Generic)
}
-- table types that I can actually use
type Post = PostT Identity
type Reader = ReaderT Identity
type Like = LikeT Identity
-- derive instances
deriving instance Show Post
deriving instance Show (PrimaryKey PostT Identity)
instance Beamable PostT
instance Beamable (PrimaryKey PostT)
deriving instance Show Reader
deriving instance Show (PrimaryKey ReaderT Identity)
instance Beamable ReaderT
instance Beamable (PrimaryKey ReaderT)
deriving instance Show Like
instance Beamable LikeT
instance Beamable (PrimaryKey LikeT)
instance Table PostT where
data PrimaryKey PostT f = PostId (Columnar f Int)
deriving Generic
= PostId . (_postId :: PostT f
primaryKey -> Columnar f Int)
instance Table ReaderT where
data PrimaryKey ReaderT f = ReaderId (Columnar f Int)
deriving Generic
= ReaderId . (_readerId :: ReaderT f
primaryKey -> Columnar f Int)
instance Table LikeT where
data PrimaryKey LikeT f = LikeId (Columnar f Int)
deriving Generic
= LikeId . (_likeId :: LikeT f
primaryKey -> Columnar f Int)
-- types for the database
data BlogDB f = BlogDB
_blogPosts :: f (TableEntity PostT)
{ _blogReaders :: f (TableEntity ReaderT)
, _blogLikes :: f (TableEntity LikeT)
,deriving (Generic)
}
instance Database be BlogDB
blogDb :: DatabaseSettings be BlogDB
= defaultDbSettings blogDb
The important thing here is that the names of the record fields for table types
must be prefixed with an underscore followed by the table’s name, otherwise
Beam won’t be able to work with the existing schema. For example, the reader
table has an ip_address
column, so the equivalent record field is
_readerIpAddress
. Foreign keys in the likes table also need special names
like post_key__id
. Having to use underscores for column names may be a bit
too opinionated, but I can live with it.
Writing queries with Beam
Once the schema is ready I can start working on the queries. The first thing to do is to insert data into the database. Here’s a function that inserts a new post row:
import Database.Beam
import Database.Beam.Sqlite
import qualified Database.Beam.Backend.SQL.BeamExtensions as BeamExt
import qualified Data.Text.Lazy as Text
insertNewPost :: Text.Text
-> SqliteM (PrimaryKey PostT Identity)
= do
insertNewPost stringId <- BeamExt.runInsertReturningList (_blogPosts blogDb)
[newPost] $ insertExpressions [ Post default_ (val_ stringId) ]
return $ pk newPost
The post table only holds the string ID of the post, so that’s the only argument
for this function. Here, the expression Post default_ (val_ stringId)
is
inserted, which just means “A new post row with an autoincrement ID, and the
provided string ID.” This whole expression thing might be a little hard to
understand, but it’s a really great way of storing rows with auto increment
fields. In popular ORMs found in imperative languages, you usually have to set a 0
or -1
to an auto increment field during insertion, and it feels like a hack
when compared to Beam’s approach. Finally, return $ pk newPost
returns the new
post’s primary key, which will be needed to insert a row to the likes table.
After inserting a few posts, they can be looked up with this function:
findPostByStringId :: Text.Text -> SqliteM (Maybe Post)
=
findPostByStringId targetStringId
runSelectReturningOne$ select
$ filter_ (\row -> _postStringId row ==. val_ targetStringId)
$ all_ (_blogPosts blogDb)
This function takes a string ID as argument and returns a record with that exact
same ID, or Nothing
if there’s none. Here you can see how Beam’s DSL starts to
shine. The three main functions are select
, filter_
, and all_
. Together
they basically tell SQLite “I want you to select all the rows from the posts
table that satisfy this filter.” The function runSelectReturningOne
wraps the
result in a Maybe
instead of the typical List
, which is exactly what I need
here because string IDs are meant to be unique.
What about looking up the number of likes for a particular post? That’s a bit
more complicated. The “likes” table is a many to many relation between unique
posts and unique readers. This implies the need for the aggregate function
COUNT
.
getPostLikesCount :: Text.Text -> SqliteM (Maybe Int)
= runSelectReturningOne $ select $
getPostLikesCount postStringId -> countAll_) $ do
aggregate_ (\_ <- all_ (_blogPosts blogDb)
posts <- oneToMany_ (_blogLikes blogDb) _likePostKey posts
likes ==. val_ postStringId)
guard_ (_postStringId posts pure likes
The do
block here is basically a query for all the rows to count. First take
all posts. Then, for every post, find all the likes that reference that post.
Finally only keep the rows whose string_id
column is the one that we are
interested in. Feeding that do
block to aggregate_ (\_ -> countAll_)
counts
all the retrieved rows. This actually gets compiled to something like:
SELECT COUNT(*) AS "res0"
FROM "posts" AS "t0"
INNER JOIN "likes" AS "t1"
ON ("t1"."post_key__id")=("t0"."id")
WHERE ("t0"."string_id")=(?);
This is the same query that I would have written if I had been using SQL directly. Awesome!
With these “basic” blocks, I can build two higher abstractions:
incrementLikesCount
, which inserts a like to a post and then returns the
updated likes count, and getLikesCount
, which counts the number of likes for
a particular post. These two functions are the ones that I expose so that they
can be used in my Scotty actions.
Using Beam queries in Scotty actions
Now that the queries are ready it’s time to use them in the Scotty actions that
handle the HTTP requests. Beam queries run on IO
, but Scotty actions run on
ActionM
, so we are going to need some lifting. Too much IO can be harmful, so I created
a new typeclass for my queries, and implemented an instance for ActionM
that
uses Scotty’s liftAndCatchIO
to run the queries.
class DBClient m where
incrementLikesCount :: Connection
-> LikeInfo -> m (Maybe Int)
getLikesCount :: Connection
-> Text -> m (Maybe Int)
instance DBClient ActionM where
=
incrementLikesCount conn likeInfo $ Q.incrementLikesCount conn likeInfo
liftAndCatchIO =
getLikesCount conn postStringId $ Q.getLikesCount conn postStringId liftAndCatchIO
getLikesCount
is fairly easy to understand. It takes a SQLite connection and
the string ID of the post whose likes count we are interested in.
incrementLikesCount
is not so obvious because its second argument is
LikeInfo
, which I haven’t talked about yet. LikeInfo
is mere record
containing all the data needed to store that like: IP address, user agent and
post ID. So, incrementLikesCount
takes a SQLite connection, and the necessary
data to insert before the likes count can be incremented.
Now that the queries can be easily called from Scotty actions we can go back to
the route handlers postLike
and getLikes
. Remember those? They handle POST /like
and GET /like
respectively. This is the final implementation
postLike
:
getNewLikeInfoFromRequest :: ActionM (Either APIError LikeInfo)
= do
getNewLikeInfoFromRequest <- getIpAddress
ipAddress <- getUserAgent
userAgent <- param "stringId"
postStringId let readerInfo = ReaderInfo <$> ipAddress <*> userAgent
return $ LikeInfo postStringId <$> readerInfo
respondWithLikesCount :: Int -> ActionM ()
= text $ pack $ show likesCount
respondWithLikesCount likesCount
postLike :: SQLite.Connection -> ActionM ()
= do
postLike conn <- getNewLikeInfoFromRequest
likeInfo $ \validLikeInfo -> do
whenValid likeInfo Just likesCount <- incrementLikesCount conn validLikeInfo
respondWithLikesCount likesCount
First, a LikeInfo
value is built from the request. getNewLikeInfoFromRequest
is an action that attempts to read all the necessary data from a request, and it
can either return an error, or the correct data. Then, I use this helper function
whenValid
that calls the passed lambda function with valid data when likeInfo
has a Right
constructor. Otherwise it sends the HTTP response with the
APIError
’s bad status code. When the request data is valid,
incrementLikesCount
is invoked and an HTTP response with plain text
containing the updated likes count is sent back to the client.
getLikes
is way simpler, here’s the implementation:
getLikes :: SQLite.Connection -> ActionM ()
= do
getLikes conn <- param "stringId"
postStringId Just likesCount <- getLikesCount conn postStringId
respondWithLikesCount likesCount
It simply reads the string ID from the URL params, and then runs the
getLikesCount
query and responds with the likes count. I don’t need to check
the value of postStringId
, because if it is not present, then Scotty
immediately throws an error and ends up returning a bad status code.
That’s all I need from this API for now. In the future I might want to add more endpoints for whatever new needs I might have. The next thing to do is to create a widget that lets the user interact with the server using these endpoints. The next post will be about how the like button at the bottom of this page was implemented with JavaScript using Mithril.