Suave.IO introduction and example - Part 3: Requests and routing

Suave.IO introduction and example - Part 3: Requests and routing

-

This post is part of a series where we create a Suave.IO-based application from scratch and run it in Azure. If you want to grab the code you can get it from the suave-todo repo in my GitHub account.

By the end of the previous post Part 2: Setting up a project we had an actual application running locally on our machine. It looked something like this.

Plain Suave.IO project ready to go

I admit, it isn't that impressive. Let's see if we can do something about that. In this post we will be expanding our application with some actual functionality. By the end of the post our application should be able to do the following.

  • Return our app (HTML)
  • Return any static content required (like JS and CSS)
  • Handle GET requests to get todos
  • Handle POST requests to add new todo items
  • Handle DELETE requests to remove todo items

Our app and API

Before we start digging into the details, let's agree on a design.

GET /

Returns our app markup

GET /static/style.css

Returns the stylesheet for our application

GET /static/app.js

Returns a tiny AngularJS app to handle things on the frontend

GET /todos

Returns all our existing todos (JSON-formatted).

POST /todos

Adds a new todo item to our list of todos.

DELETE /todos/{id}

Deletes an item from our list of todos.

Let's do some routing

Now that we know what we want to achieve we can start setting up some routes and start handling requests. Remember from the last post that we instructed the Suave.IO webserver to respond to requests with the value of app?

let app = OK "Welcome to the todo app"

Rather than just return the same string for every request we instead want to change app to handle different routes and respond to them differently.

What is "OK"?

To understand how we can achieve that, let's start by discussing what "OK" actually is. Not surprisingly, "OK" is a function, which is of a type called WebPart.

A WebPart is a function that takes a HttpContext and asynchronously returns an optional HttpContext. From the Suave.IO documentation:

type WebPart = HttpContext -> SuaveTask<HttpContext>
// hence: WebPart = HttpContext -> Async<HttpContext option>

Our Suave.IO webserver simply expects to get a WebPart, as long as we give it a WebPart it will be able to serve requests.

When a request is made to the server it will then call the WebPart function we have provided it with, and if it returns a result (remember it is optional, so it can return "Some HttpContext" or "None") a response can be returned to the client.

Composing WebParts

Rather than just giving it an "OK"-webpart that responds to all requests with the same Status 200 response, what if we could give our webserver a WebPart that is composed of different WebParts handling different situations?

As it turns out, we can!. Let's look at an example of a composed WebPart.

let app : WebPart =
  choose
    [ path "/" >>= OK "Welcome!"
      pathScan "/post/%d" (fun (id) -> OK(getById id)) ]

The value of app is still a WebPart, and can be called by our webserver to respond to requests. But it's composed of different WebParts.

First the WebPart-function "choose" is called, it iterates a list of other WebParts and returns the first result it finds (when a WebPart in the list returns "Some HttpContext" rather than "None").

The WebParts in the list are themselves composed. For example.

path "/" >>= OK "Welcome!"

This is a composition of two WebParts, the first of which is "path". If the path matches (we are requesting "/") we will call the next WebPart, which is "OK", and which returns a result. If the path doesn't match, the "path" WebPart will return None, and "choose" will continue to search the list for a result.

This way we can build, or compose, a WebPart-function for our WebServer that can respond differently depending on the request.

If you want a more in-depth explanation of WebPart composition and routing I suggest you read the Suave.IO documentation and Railway oriented programming as well.

Todo routing

Now that we have a basic concept of how we do routing in Suave we can attempt to compose a WebPart that routes according to our app and API design. Let's jump right into it.

let app : WebPart =
    choose 
        [ GET >>= choose
            [ path "/static/app.js" >>= Writers.setMimeType "application/javascript" >>= OK script
              path "/static/style.css" >>= Writers.setMimeType "text/css" >>= OK style
              path "/" >>= OK html 
              //pathScan "/static/%s" (fun (filename) -> file (sprintf "./static/%s" filename)) 
              path "/todos" >>= request (fun req -> OK (getTodos ())) ]   
          POST >>= choose
            [ path "/todos" >>= request (fun req -> add (req.formData "text") ; OK "") ]
          DELETE >>= choose
            [ pathScan "/todos/%d" (fun (id) -> remove id ; OK "") ]       
        ]

Route hiearchy

Here we have everything we need to serve all the routes specified by our design. Let's take a closer look to see what's going on.

At the very root of our route WebPart composition we have "choose", which will try to find a match between three "child"-WebParts: GET, POST and DELETE. These WebParts obviously handle different HTTP request methods.

In case we are doing a GET-request, choose will look for a match in the list of WebParts "under" GET. If we are doing something other than a GET-request, the first WebPart is skipped, and the rest of the list is searched for a match (POST or DELETE).

Paths

In nearly all cases above we match against an exact path using the "path"-WebPart. In the case of our stylesheet we look for an exact location, then we add the mime type before returning the contents of a value named "style".

path "/static/style.css" >>= Writers.setMimeType "text/css" >>= OK style

Path scan

Quite often we will be interested in data that is embedded in the path. In that case we can use the "pathScan"-WebPart and the same patterns we use for string formatting. When deleting todo-items we specify the Id of the element to delete as part of url, and use pathScan to retrieve it.

pathScan "/todos/%d" (fun (id) -> remove id ; OK "")

Notice that we just return a 200 response without content after removing the todo item.

Serving static content

I've added all my static content as values in a separate script called "static.fsx" which I then reference. So when one of the static files are requested we actually return a hard-coded value.

path "/static/style.css" >>= Writers.setMimeType "text/css" >>= OK style

Another (slightly more flexible) approach would to use the "file"-WebPart to load and return the contents of an actual file on the server. Perhaps doing something like this.

pathScan "/static/%s" (fun (filename) -> file (sprintf "./static/%s" filename))

The reason I use hard-coded values for this example is because the "file"-WebPart causes trouble when running the application in Azure which I will be doing later in the series. I haven't yet figured out exactly why.

Access to the request

You will notice I use the "request"-WebPart in a couple of places, most importantly in the path for adding new todo items.

path "/todos" >>= request (fun req -> add (req.formData "text") ; OK "")

Using the "request"-WebPart gives you access to the details of the request, in this case I use it to get access to the posted form values, so I can get the the text of the new todo-item.

I also use the "request"-WebPart when a list of todos is requested. I do this to make sure the function returning the todo-items is called on all requests rather than just on the first request (since we want a different response when the data changes).

path "/todos" >>= request (fun req -> OK (getTodos ()))

But does it work?

Now we've looked at the routing, let's wrap up this chapter by taking a look at the entire application so far (the frontend code isn't important to this series, if you want to check out you will find it in the GitHub repo).

First, here is a screenshot of the application (beautiful right?).

Suave todo screenshot

And here is the entire backend logic of the application.

#r "packages/Suave/lib/net40/Suave.dll"
#load "static.fsx"

open Suave
open Suave.Web
open Suave.Http
open Suave.Types
open Suave.Http.Successful
open Suave.Http.Redirection
open Suave.Http.Files
open Suave.Http.RequestErrors
open Suave.Http.Applicatives
open Static

let mutable id = 0
let mutable todos = []

let getTodos () =
    todos
    |> List.map (fun t -> sprintf "{ \"id\": %d, \"text\": \"%s\" }" (fst t) (snd t))
    |> String.concat ","
    |> sprintf "{ \"todos\": [ %s ] }" 

let add (text : Choice<string, string>) =
    match text with
    | Choice1Of2 t -> 
        let next = id + 1
        id    <- next
        todos <- (id, t) :: todos
        ()
    | Choice2Of2 t -> 
        ()

let remove id = 
    let removed =
        todos
        |> List.filter (fun t -> (fst t) <> id)
    todos <- removed
    ()

let app : WebPart =
    choose 
        [ GET >>= choose
            [ path "/static/app.js" >>= Writers.setMimeType "application/javascript" >>= OK script
              path "/static/style.css" >>= Writers.setMimeType "text/css" >>= OK style
              path "/" >>= OK html 
              //pathScan "/static/%s" (fun (filename) -> file (sprintf "./static/%s" filename)) 
              path "/todos" >>= request (fun req -> OK (getTodos ())) ]   
          POST >>= choose
            [ path "/todos" >>= request (fun req -> add (req.formData "text") ; OK "") ]
          DELETE >>= choose
            [ pathScan "/todos/%d" (fun (id) -> remove id ; OK "") ]       
        ]

That's fine right? Well, it has a couple of drawbacks. First of all we are not persisting our todo-items. Also, you are probably offended by the use of mutable values for storing items and the Id-counter.

What happens next

So in order to address these issues we should probably consider persisting our todo-items using some sort of database. Check out the next part of the series Part 4 - Database access for an example of how we can do that.

View Comments