Server-side HTML Rendering with F# and Giraffe.ViewEngine
Date: 2023-12-13 | create | tech | fsharp | giraffe | html |
I've been experimenting with different paradigms for server-side html rendering with F#.
- Scriban Templates: Type-safe Server-side HTML Rendering with F# / Giraffe
- Raw string interpolation: Build a Simple Markdown Blog with F# / Giraffe
But both of these approaches have drawbacks that prevent them from utilizing the full power of F#.
- Scriban Templates - Sacrifices build-time type safety as data population errors won't be hit until run-time
- Raw string interpolation - More type-safe but you can't nest verbatim strings which means you often need to split up markup generation across many locations, sacrificing logic locality
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?
Answer
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#.
- Type-Safety - Everything is F# (including the markup!) so you get the full power of the type system and its build-time checks
- Markup Locality - Because we are now using pure F# (as opposed to nested strings), we are able to do all our conditionals / iterations in-line just as we would with normal lists. This allows for more localized markup which can be easier to read / maintain if you're okay with the DSL style.
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.
- App: F# / Giraffe - rendering HTML with Giraffe.ViewEngine
- Data: Entity Framework with Postgres
- Hosting: Docker and Docker Compose
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
Installation
The first thing we need to do is install the library.
- GitHub - Giraffe.ViewEngine
- Nuget - Giraffe.ViewEngine
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.
Rendering a Page
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)
- Attributes - for things like classes, ids, etc
- Children - for nesting things
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:
- mainPageHttpHandler - Our entrypoint from Giraffe which will process any context we need and call the main render function
- renderMainPageAsync - Creates a new random item, fetches random items from its db, and builds up the html page
- renderSentinelTableComponent - Where we actually render the html table
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
}
}
Serving HTML with Giraffe
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:
- renderView - A helper middleware from converting from Giraffe.ViewEngine's
XMLNode
representation to the string html representation - constructEndpoints - Creates an endpoint list we can register in Giraffe. Here we just have one endpoint
/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
)
)
]
]
Next
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:
Want more like this?
The best / easiest way to support my work is by subscribing for future updates and sharing with your network.