Routing In Suave.io - Web Development With F#

This article is the continuation of the Suave.io introduction that you can find at the following link.

A web application is generally composed of multiple “routes”.  A route means a URL that does not necessarily map a physical file. You can use routes to define some URLs that are semantically meaningful to the user.  

To keep it simple, routes are basically paths the user takes which are attached to the code that will be triggered when a user reaches the specific route.

Choose different routes

In the last article, we had a basic routing system, like following.

  1. let webPart =    
  2.     choose [    
  3.         path "/" >=> (OK "Home")    
  4.         path "/about" >=> (OK "About")    
  5.     ]   
  6.   
  7. startWebServer defaultConfig webPart  

It's up to you to define your routes according to the needs of your application; a route returns a WebPart as a response. I will stay on simple examples in this article but the same rules apply in complex applications as well.

The choose function has the following signature.

  1. val choose: WebPart<'a> list -> WebPart<'a>  

It takes a list of WebPart options in parameter and returns a single WebPart value corresponding to the route the user selected. Note that the WebPart value in return can be Some (the first one that applies) or None if the requested route does not correspond to any WebPart.

Path is a function that defines a filter corresponding to a matching string. It takes a string as a parameter and returns a WebPart value. Like I said in previous article, >=> is a Suave operator with the following signature

  1. val ( >=> ): a: 'a -> Async<'b option> ->b: 'b -> Async<'c option> -> 'a -> Async<'c option>  

That takes two WebParts and composes the left WebPart to the right WebPart in one WebPart. The WebPart on the left is evaluated and applied only if Some is returned.

Now, let’s add some routes in our list.

  1. let webPart =    
  2.     choose [    
  3.         path "/" >=> (OK "Home")    
  4.         path "/about" >=> (OK "About")  
  5.         path "/articles" >=> (OK "List of articles")  
  6.         path "/articles/browse" >=> (OK "Browse articles")  
  7.         path "/articles/details" >=> (OK "Content of an article")  
  8.     ]   

If you launch the web server, you can now navigate to your routes.

F#

For now, the defined routes remain very basic and static. We will see how to allow interactions through URLs, which will help to render our application dynamic.

URL parameters 

URL parameters enable the user to use parameters in the request. For example, access a specific article in a blog with the ID. Suave.io integrates typed routes functionality that means we can control arguments for the route in the code. An argument can have any basic type like an integer or a string.

Suave.io brings some formatters to detect the argument types and filter them. It looks like formatters, like in the printf function from the C standard library. For example, to cache an integer, we use %d or we use %s to cache a string.

Let’s integrate this to enable an id in the details route but before that, I would like to introduce the pathScan function. pathScan is similar to path and has the following signature:

 

  1. val pathScan:  PrintfFormat<'a,'b,'c,'d,'t> -> 't -> WebPart -> WebPart  

 

It takes two parameters,

  • The first parameter is statically typed string formatting function
  • The second parameter is a function that takes type of the parameter of the formatted string (‘t) and returns a WebPart.

Edit the current path of details of the article, like this,

  1. pathScan "/articles/details/%d" (fun id -> OK (sprintf "Content of the article with ID: %d" id))  

In the first parameter, we specify the format %d that means we want an integer, and then, we recover it in the lambda expression with id parameter, then we format it to display into a string on the page.


F#

This shows the power of the F# language and functional programming: a project is composed of a set of functions glued together to build the application very simply and properly.

It is possible to put more than one parameters.

  1. pathScan "/articles/%s/%d"  
  2.             (fun (c, id) -> OK (sprintf "Category: %s; Id: %d" c id))  

All successive parameters are retrieved in a tuple in the lambda expression. A tuple in F# is a grouping of unordered values that can be of any type. It is used here for getting all the parameters of different types and work with them in the lambda body expression.

F#

Here is the current code.

  1. open Suave                 // always open suave  
  2. open Suave.Successful      // for OK-result  
  3. open Suave.Web             // for config  
  4.   
  5. open Suave.Filters  
  6. open Suave.Operators  
  7.   
  8. let webPart =    
  9.     choose [    
  10.         path "/" >=> (OK "Home")    
  11.         path "/about" >=> (OK "About")  
  12.         path "/articles" >=> (OK "List of articles")  
  13.         pathScan "/articles/%s/%d"  
  14.             (fun (c, id) -> OK (sprintf "Category: %s; Id: %d" c id))  
  15.         path "/articles/browse" >=> (OK "Browse articles")  
  16.         pathScan "/articles/details/%d"  
  17.             (fun id -> OK (sprintf "Content of the article with ID: %d" id))         
  18.     ]   
  19.   
  20. startWebServer defaultConfig webPart  

Query parameters 

Another way to use URL address to pass information is query parameter. A query parameter looks like,

http://127.0.0.1:8080/articles/browse?genre=fsharp

Where genre is the parameter’s name and fsharp the value. You can combine multiple parameters using the ‘&’ character.

http://127.0.0.1:8080/articles/browse?genre=fsharp&nb_article=10

For handling query parameters, we will use the request function that takes in parameter a function of type HttpRequest -> WebPart. As an example, to get a parameter of name “genre”, we can do something like.

  1. path "/articles/browse" >=> request (fun r ->   
  2.                                        let genre =  
  3.                                            match r.queryParam "genre" with  
  4.                                            | Choice1Of2 genre -> genre  
  5.                                            | Choice2Of2 msg -> BAD_REQUEST msg)  

R argument in lambda represents the HttpRequest and contains query parameters.

F#

If we look at the queryParam type definition, it is -

  1. val pathScan:  PrintfFormat<'a,'b,'c,'d,'t> -> 't -> WebPart -> WebPart 

 

This is a member function that takes a string in parameter. In the code above, “genre” is corresponding to the name of a query parameter. It returns a Choice type that represents a choice between two types. This is a more complex notion, for now, just let me tell you that the first match stands for a value of the query parameter and the second match stands for error message, like "parameter with the given key was not found in query". Choice can manage all possible cases.

We can imagine a case when we can have multiple query parameters.

  1. path "/articles/browse" >=> request (fun r ->   
  2.                                             let genre = match r.queryParam "genre" with  
  3.                                                         | Choice1Of2 genre -> genre  
  4.                                                         | _ -> ""  
  5.                                             let limit = match r.queryParam "limit" with  
  6.                                                         | Choice1Of2 limit -> int limit  
  7.                                                         | _ -> 0  
  8.                                             OK (sprintf "Genre: %s and number of articles limit: %d" genre limit))  

F#

Be careful, a pattern matching should always return the same type in all cases!

The code shown in the article can be found here.