Date: 2023.12.13 | create | fsharp | giraffe | html | tech |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
I've been experimenting with different paradigms for server-side html rendering with F#.
But both of these approaches have drawbacks that prevent them from utilizing the full power of F#.
HTML DSLs like Giraffe.ViewEngine offer an alternative paradigm for HTML rendering which leans into F#, theoretically solving for both issues. In this post we'll explore building simple HTML pages with Giraffe.ViewEngine.
Q: Do F# HTML DSLs like Giraffe.ViewEngine offer better HTML Devx?
Overall I think Giraffe.ViewEngine offers an excellent balance between type-safety, devx, and performance (see: F# Giraffe.ViewEngine Benchmarks). The DSL approach to building HTML won't be for everyone but does offer some significant perks should you choose to go all-in on F#.

In the rest of this post we'll be building a simple HTML page that renders a table of random data, populated every time you hit the page. This will offer a simple demonstration for how Giraffe.ViewEngine works for rendering basic HTML.
This stack is overkill for this demonstration but offers a semi-realistic environment for rendering HTML. We'll be focusing on the Giraffe.ViewEngine rendering but the full project source code is available in the HAMY LABS Example Code Repo, available to all HAMINIONs subscribers.
If you've never used Giraffe before, you might want to check out: Build a simple F# web API with Giraffe
The first thing we need to do is install the library.
You can install it with:
dotnet add package Giraffe.ViewEngine
Note: Giraffe.ViewEngine has not been updated in years. In most main stream languages this is a problem as the ecosystem will likely have weathered several breaking changes. In dotnet-land this is commonly not a problem (at least since .NET standard circa 2019). Many packages approach "completeness" where they do their job and do it well and thus do not need any updates. My professional opinion is that Giraffe.ViewEngine is one of these packages - it hasn't been updated in awhile but it also doesn't really need anything.
Now that we have the package, let's talk through rendering a simple HTML page with Giraffe.ViewEngine.
Giraffe.ViewEngine works by building up nested lists of XMLNodes. Each XMLNode represents a different HTML tag (like a div, or table, or li, etc). Once you've built up your lists of XMLNodes you can convert it to an html string so it's recognizable as html and renderable by the browser.
Most tags will take in two lists (though some may only take in one if they don't support children)
A simple example might be:
div [] [
h1 [] [ Text "iamanh1" ]
]
This creates a div with a child h1.
<div>
<h1>iamanh1</h1>
</div>
This indirection may be off-putting as it's a further abstraction from the underlying HTML model. But it does have some advantages in devx as we touched on earlier and is incredibly fast compared to many other approaches I've tried.
To render our html, we have:
module MainPageView =
open Giraffe.ViewEngine
type SentinelTableComponentProps =
{
Sentinels : Sentinel list
}
let renderSentinelTableComponent
(props : SentinelTableComponentProps)
: XmlNode
=
let giraffeTemplate =
table [] (List.concat [
[
tr [] [
th [] [
Text "ID"
]
th [] [
Text "Data"
]
];
];
(
props.Sentinels
|> List.map (
fun s ->
tr [] [
td [] [ Text s.id ]
td [] [ Text s.data.name ]
]
)
)
])
giraffeTemplate
type MainPageProps =
{
SentinelCount : int
}
let renderMainPageAsync
(serviceTree : SentinelServiceTree)
(props : MainPageProps)
: Async<XmlNode>
=
async {
let! sentinelResult =
sendGetSentinelsQueryAsync serviceTree { count = props.SentinelCount }
let sentinels =
match sentinelResult with
| Ok s ->
s
|> Seq.toList
| Error s -> raise (System.SystemException("Failed to get Sentinels"))
do!
sendCreateSentinelCommandAsync serviceTree
|> Async.Ignore
let giraffeTemplate =
html [] [
head [] [
meta [
_charset "UTF-8"
]
title [] [ Text "Sentinels Table" ]
]
body [] [
main [] [
h1 [] [ Text "Sentinels Table" ]
(renderSentinelTableComponent { Sentinels = sentinels })
]
]
]
return giraffeTemplate
}
let mainPageHttpHandler
(sentinelServiceTree : SentinelServiceTree)
=
fun (next : HttpFunc) (ctx : HttpContext) ->
async {
return!
renderMainPageAsync
sentinelServiceTree
{
SentinelCount = 10
}
}
Now that we have our HTML rendering capabilities, we need to actually hook up the endpoint so it can return the HTML table. This is pretty simple and just requires a few lines of code.
Going into detail about how Giraffe works is beyond the scope of this post, so if you want to learn more checkout:
In this code we:
XMLNode representation to the string html representation/sentinels that calls our renderView helper on the return of our page renderer (above)module SentinelEndpoints =
open Giraffe.ViewEngine
let renderView
(handler : HttpFunc -> HttpContext -> Async<XmlNode>)
: HttpHandler
=
fun(next : HttpFunc) (ctx : HttpContext) ->
task {
let! result = handler next ctx
let resultString =
result
|> RenderView.AsString.htmlDocument
return! htmlString resultString next ctx
}
let constructEndpoints (serviceTree : SentinelServiceTree) =
[
GET [
route "/sentinels" (
renderView (
MainPageView.mainPageHttpHandler serviceTree
)
)
]
]
That's it - a simple HTML page with Giraffe.ViewEngine. Originally I was against using DSLs for HTML but as I've used it more I've begun to warm up to its devx and performance benefits.
If you liked this post, you might also like:
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.