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