United Kingdom: +44 (0)208 088 8978

Why we love SAFE stack: more Giraffe and Saturn

In continuation of our blog series on Safe Stack, we finish our earlier explorations of Giraffe and take a look at Saturn, a framework that makes Giraffe even easier to work with

We're hiring Software Developers

Click here to find out more

In an earlier post in this series, we had a preliminary look at Giraffe: we discussed the httpHandler, the main building block of a Giraffe application, and showed how we can use Giraffe to add an extra API to a SAFE app. In this post, we have a closer look at some of our favorite Giraffe features, and look into Saturn, a framework built on top of Giraffe.

Giraffe

Auth

Authorization and authentication can be a major headache when building anything web-connected. Giraffe has your back with some very simple handlers that can check whether someone is authorized. This is all powered by ASP.NET Core's authentication capabilities, so we can trust that its underlying security is tested at enterprise level. Let's have a look at the authorizeUser handler.

It takes two arguments: a policy, and an authFailedHandler. The policy is a function that takes a claims principal and returns a boolean. This is where you validate whether the user is authorized to do what they are trying to do. The authFailedHandler is a HttpHandler like any other used in Giraffe. it is called when authorization fails. Since this is just another handler, you can do whatever you want here, like redirecting to an authentication page. Here's an example of how you'd lock out under-aged users from spicy web pages:

authorizeUser (fun cp -> cp.HasClaim (fun c -> c.Type = "age" && c.Value > "18") ) (setStatusCode 401)
>=> text "The best chili con carne recipe you'll ever come across!"

The authorizeUser handler is only one of many handlers that Giraffe provides to aid you in authentication, such as requiresRole for role-based authorization and SignOut, which helps you set up logout flows for various auth schemes.

Deserialization

Giraffe also has some tools to help us with deserialization of JSON, XML, or HTTP forms. Let's have a quick look at deserializing some JSON:

[<CLIMutable>]
type MyModel =  {
    Name: string
    Age: int
}

let myJsonEndpoint =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        task {
            let! myData = ctx.BindJsonAsync<MyModel>()

            match myData.Name, myData.Age with
            | null, 0 -> return! RequestErrors.BAD_REQUEST "Some data is missing!" next ctx
            | name, age -> return! Successful.OK $"Hello {name}!, you are {age} year old" next ctx
        }

You provide the generic method BindJsonASync<'T>() with a model that you want to deserialize to, and it will take the request body and deserialize the JSON! An important note here is that the serializer is not super critical about the JSON it takes in, and will simply leave null values for nullable strings or default values for primitive types when the corresponding values are not provided. Don't forget to do a sanity check of your data before proceeding!

The HTTP form equivalent does have a safer version, TryBindFormAsync<'T>(), which will return None if fields are missing.

Saturn: Giraffe made eas(y/ier)

On top of Giraffe we use Saturn, a framework that trades off some of the conceptual simplicity and verbosity of Giraffe for simple syntax and sensible defaults. We like it a lot because it makes setting up a Giraffe app a bit simpler. Saturn makes extensive use of computation expressions.

use_static

The default SAFE template mainly makes use of Saturn's application computation expression. Apart from hooking up the Giraffe webApp, it adds a couple of settings. Let's look at the use_static CE operation.

Apart from the dynamic content served up in our Giraffe app, we also want to serve up static files like our JavaScript bundle. ASP.NET Core has middleware that takes care of that, but setting it up is not trivial. Saturn takes care of it for you, and does it in a single line!

To illustrate the win in reduced complexity on our side, this is what use_static does under the hood:

[<CustomOperation("use_static")>]
member __.UseStatic(state, path : string) =
  let middleware (app : IApplicationBuilder) =
    match app.UseDefaultFiles(), state.MimeTypes with
    |app, [] -> app.UseStaticFiles()
    |app, mimes ->
        let provider = FileExtensionContentTypeProvider()
        mimes |> List.iter (fun (extension, mime) -> provider.Mappings.[extension] <- mime)
        app.UseStaticFiles(StaticFileOptions(ContentTypeProvider=provider))
  let host (builder: IWebHostBuilder) =
    let p = Path.Combine(Directory.GetCurrentDirectory(), path)
    builder
      .UseWebRoot(p)
  { state with
      AppConfigs = middleware::state.AppConfigs
      WebHostConfigs = host::state.WebHostConfigs
  }

Controller

The controller lets you create CRUD controllers with ease, without having to deal with routing! Let's give it a go by adding some endpoints to read and write Todo's in the default SAFE TODO demo.

The computation expression has various custom operations that set up different routes. In this example, I'll implement the index, create and show operations. They will automatically be routed to some standard routes. I added some comments to illustrate which handlers deal with which routes.

let todoController =
    subRoute
        "/todos"
        (controller {
            // GET /todos; lists all todos
            index (fun ctx -> Storage.todos |> Controller.response ctx)

            // POST /todos; adds a new todo
            create (fun ctx -> task {
                let! content = Controller.getModel<{| Description: string |}> ctx
                let todo = Todo.create content.Description

                return
                    match Storage.addTodo todo with
                    | Ok() -> setStatusCode 200
                    | Error e -> setStatusCode 500
            })
            // GET /todos/{todo_id}; get information on a single todo
            show (fun ctx id ->
                let todo = Storage.todos |> Seq.tryFind (_.Id >> (=) id)

                match todo with
                | Some t -> Controller.response ctx t
                | None -> setStatusCode 400 earlyReturn ctx)
        })

Content negotiation

Let's look a bit closer at the index operation: it's very simple: we pull the todo's out of storage, and pass them into the Saturn Controller.response function. The controller function returns data based on the Accept header if it's available, or based on the Content-Type header if this is not available. For data, the supported types are XML and JSON, meaning that our endpoint can now return either of those data types, depending on the consumer's preference! In the create function, we use Controller.GetModel which deserializes based on the Content-Type header. Nifty!

When using this, make sure you test both the JSON and XML (de)serialization: the serializers work differently, and a type one serializer can deal with, can cause problems in the other.

Because the HttpHandler returned by the controller is always used as the last handler in a chain, we are not provided with a next handler. You can see how that might create an issue in calling the SetStatusCode handler in the show function, which expects next and ctx as arguments (check out the Giraffe: The Basics blog post). Giraffe provides a HttpHandler called earlyReturn. Calling earlyReturn instead of next can be used to call a handler as the final handler in the chain, not giving any later handlers a chance to handle the request.

Apart from these simple operations, the computation expression also provides the possibility to add sub-controllers, allowing you to create nested paths like todos/{todo-id}/subtasks. Check out the documentation for more on that, and other features of the computation expression.

Conclusion

As you can see, the server-side technologies used in the SAFE stack pack a punch! While for a lot of apps you can fully rely on fable remoting as the sole API of your application, you have the option to add your own REST APIs too. If you feel like both APIs in a single server gets too big, or you just want a server without a client, check out Saturn's getting started guide to build a stand-alone Saturn app