Building a Blog in Haskell with Yesod–JSON API

Posted on August 19, 2019 by Riccardo

This is a series about Yesod: a Haskell web framework that follows a similar philosophy to Rails. In fact, it is strongly opinionated and provides a lot of functionality out of the box.

A good read about Yesod is available online for free: Developing web applications with Haskell and Yesod. That’s why this series will be a commentary of the commits from a repo we will use to develop a super simple blog.

In other words, this won’t be good material to learn how to use Yesod. However, it will hopefully give an overview of how the framework works.

Series index:

Back in Business

Last week’s post started on a bitter note:

The plan for this post was to transform the entire blog into an API. Unfortunately, the compiler got in the middle.

It’s not a secret that the Haskell compiler can be at times difficult to satisfy. However, what a joy it is when the program finally type checks! This time we got it covered. So let’s dive into it.

Authentication

Last week’s post was a bit of a lie. In fact, since the authentication wasn’t taken care of, the user had to adhere to the following steps to get the list of posts in JSON format:

  • visit /api/posts on a browser
  • the application would have shown the login form
  • submit the login form
  • visit /api/posts on a browser again

That is because the user didn’t have any means to authenticate the /api/posts requests. Therefore, they had to create a session (cookie) by logging in.

Commit cd78427e82babef42f170bf7b3e4ff423d88a729 fixes that by patching maybeAuthId:

The default maybeAuthId

Retrieves user credentials, if user is authenticated.

By default, this calls defaultMaybeAuthId to get the user ID from the session.

The docs go on saying

This can be overridden to allow authentication via other means, such as checking for a special token in a request header. This is especially useful for creating an API to be accessed via some means other than a browser.

That is why, the patched code above looks for the id in the X-User-Id header and delegates to the default behaviour when not found.

Registration

Up until now we’ve been using Yesod.Auth.Dummy for both authentication and registration. In fact, Yesod.Auth.Dummy renders a login form with one text field. If it’s submitted with the username of an existing user, then that becomes the logged in user. Otherwise, it first adds a new record to the database and then creates the session.

Unfortunately, Yesod.Auth.Dummy does not support registration via JSON requests. Therefore, we have to patch it ourselves.

Commit 78e13f807f5674aa0a84e2633d7967fa02b755cf just copy / pastes the authDummy code from Yesod.Auth.Dummy. The real change is done in commit cd78427e82babef42f170bf7b3e4ff423d88a729.

Firstly, we introduce a parser

that is capable of extracting ident from a JSON like the following

Secondly, we patch the dispatch function of the authDummy plugin. That is the place in charge of taking care of the POST requests to /auth/page/dummy. Up until now, it only worked when the html form was submitted. To make it work with a JSON request we need to make a few changes:

In more detail, we first try to extract ident from the POST params (i.e. form submission) with runInputPostResult. If that succeeds (i.e. FormSuccess) we leave the original behaviour intact.

If it fails, we try to get the JSON body with parseCheckJsonBody. If that fails we return invalidArgs (i.e. HTTP 400). If it succeeds, we try to extract from the JSON body the ident Text. If that succeeds we do the same thing as when the POST params succeed.

Create a New Post

It turns out, many concepts used in the previous section apply to creating a new post. We do that in commit 4922c8db2706fd0999ef478373b82326f5851b4d:

Delete a Post

This is the easiest part, look how small commit 166aa28741fe532ae664308a2af4b28638b6d560 is!

The only thing worth mentioning is the return Null in

That is just a shortcut to return an empty JSON body.

CURLing

Register:

Un-authenticated request:

List of posts:

Create a new post with missing parameter:

Create a new post:

Delete a post when not owner:

Delete a post:

Outro

Here we have our JSON API. This has been quite a struggle. But what a great feeling when it finally compiled!!!