Type-safe Server-side HTML Rendering with F# / Giraffe

Date: 2023-11-29 | create | tech | fsharp | giraffe | html |

For the past several years I've experimented with different tech stacks with the goal of making building apps more simple and fun. CloudSeed is my best answer so far - an F# web API that can interface with the frontend technology of your choice. This allows most of the app to be built in my favorite language F# while allowing flexibility to use whatever tech you like for your frontend.

The problem is that all apps really need a frontend. So by not supplying a rock-solid default, users must bring / figure out their own. CloudSeed currently defaults to SvelteKit - my current top choice for simple, flexible frontends (I like SvelteKit over React-based projects). But not everyone likes / knows SvelteKit so they either have to integrate their own or learn a new framework. Even if they do like and know SvelteKit, there's still the problem of context switching between the two tech stacks.

Recently I've been reading up on the hype around HTMX and have undergone somewhat of a brainblast exploring its possibilities. It promises all the simplicity and power of server-side rendering with the reactivity of a clientside framework (a la Svelte, React, Vue, etc). This seems like it could solve my problem - allowing me to build 3S (Simple Scalable System) fullstack apps with 90% of the code in F# all while sacrificing little in the way of frontend power.

In this post we're going to explore the first part of this journey - server-side HTML rendering with F# / Giraffe.

Q: How can we build type-safe server-side HTML rendering with F# / Giraffe?

Answer

In this post we'll explore an example app built with F# / Giraffe. It renders a simple HTML table displaying a list of data from its database.

This example will show you how to build:

  • Type-safe HTML web components
  • Simple, performant HTML templating
  • Integration with Giraffe for routing

The app itself:

  • App: F# / Giraffe
  • UI: HTML Templates with Scriban, served via Giraffe
  • Data: DBUp for migrations, Entity Framework for ORM, Postgres DB
  • Host: Docker / Docker-Compose for containerization and orchestration

In this post, we'll specifically be diving into the App / UI layers (including important source code). The rest is just there to serve as a "tracer bullet" to prove this works in a fullstack system.

To learn more about the rest of the stack, read:

The full source code of this example app is available:

HTML Templates

The first thing we want is an ability to do HTML templates.

Why?

Well we could just use raw string concatenation (and many people do). In most cases this is fine.

BUT there are some edge cases where this could cause unexpected problems like if you try to use a special character like &, <, > etc. These are all reserved characters by the html spec so you'd need to first translate them into a different form (their entity name / number) if you want the browser to render them as text instead of trying to render it as html.

With raw string concatenation you'd just need to remember to parse these special characters into their correct form. This is doable but it seems like a pit of failure. The default is to forget so you need extra work to remember and not fail.

By starting with everything as a template, it's clear we need to do the HTML parsing anyway which takes care of this whole failure case for us (a pit of success). Plus most templating languages give us some nice tools for conditionals, iterations, and value usage so that's a plus.

Picking an HTML Template library

For this example app, I chose to go with Scriban. I liked Scriban because it is:

  • Popular - Currently Scriban is the second-most popular HTML templating library (though it also does a lot of other stuff) behind RazorEngine and I really wanted to stay away from the magical code generation of Razor.
  • Maintained - Latest updates are a few months ago
  • Simple - The API is nice and clean w/o any magical code gen steps. This means it should fit nicely in the FPish style of F#. Also its templating language is very simple and similar to that of Svelte / Handlebars / Liquid so easy to onboard to.
  • Fast - The repo claims its fast and I ran my own benchmarks with regular-sized templates rendering in < 1ms

You can install the package via nuget (Scriban Nuget):

dotnet add package Scriban

Once installed, we can utilize Scriban to parse our html templates and fill it with type-safe data models with a helper function like this:

let buildHtmlTemplateParser<'TTemplateProps> 
    (template : string) 
    = 
    let compiledTemplate = Scriban.Template.Parse(template) 

    match compiledTemplate.HasErrors with 
        | true -> raise (System.SystemException($"Failed to parse template {compiledTemplate.Messages}"))
        | false -> ()

    fun (props : 'TTemplateProps) -> 
        compiledTemplate.Render(
            props,
            memberRenamer = fun m -> m.Name)

This will:

  • Compile our template and check for errors
  • Return a function that takes in props and renders the HTML

We'll use this more in the next section

Building Type-safe HTML Components

Now that we have a way to parse and render our HTML strings, let's look at how we can build a page with "components" similar to how you might split up rendering concerns in a clientside framework.

We're basically trying to build a simple table that pulls in data from our DB and renders it. For this example, the data is just a giant list of Sentinels - a model that has a few strings in it.

Sentinel Table Example

To render this, we'll build:

  • A base HTML Layout
  • A child component to render the SentinelTable

Let's start with the SentinelTable. We can build this pretty simply by creating a new function renderSentinelTableComponent that takes in our Sentinel list and returns the HTML string. While the string itself isn't very type-safe, the input Props are - utilizing the full power of the F# type system.

type SentinelTableComponentProps = 
    {
        Sentinels : Sentinel list
    }

let renderSentinelTableComponent
    : SentinelTableComponentProps -> string
    = 
    let template = 
        """
        <table>
            <tr>
                <th>ID</th>
                <th>Data</th>
            </tr>
            {{ for sentinel in Sentinels}}
                <tr>
                    <td>{{ sentinel.id }}</td>
                    <td>{{ sentinel.data.name }}</td>
                </tr>
            {{ end }}
        </table>
        """
    
    let parser = buildHtmlTemplateParser<SentinelTableComponentProps> template 

    fun 
        (props : SentinelTableComponentProps) 
        ->
        // HAMY: The template is cached as we know it is static
        parser props

In the above code:

  • Define a type for the component props
  • Create a component function that:
    • Holds the HTML template
    • Creates a Scriban parser with the helper function we built in the previous section
    • Returns the rendered html from combining the template with the input Props

Looking at it this way, it's very similar to how you might build a React functional component. But this is all done serverside, right in F#!

Note: In this example, we're caching the parsed template because I wasn't sure how much of a performance impact re-parsing every time would cause. My Scriban benchmarks indicate that re-parsing takes about 0.2 ms which in most UI usecases is negligible so I'd recommend not caching unless you need it.

Next we'll build our main page which will utilize this component to render its table of Sentinel values.

As this is the root page, this is probably where you'd want to do any sorts of db accesses to build up page data the UI components will render. The SentinelCount props it receives comes directly from the F# / Giraffe endpoint we'll look at in the next section. This is similar to the SSR style of SvelteKit and NextJS with server-side data loading.

We are using simple string concatenation to insert the SentinelTableComponent into our HTML body. I think this is pretty simple, easy to read, and safe-ish as we know its doing safe HTML parsing internally but I'd be curious if anyone has suggestions for more type-safe ways to do this.

type MainPageProps = 
    {
        SentinelCount : int
    }

let renderMainPageAsync (serviceTree : SentinelServiceTree) (props : MainPageProps) = 
    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 template = 
            $"""
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <title>Sentinels Example</title>
            </head>
            <body>
                <main>
                    <h1>Sentinels Table</h1>  
                </main>
                {(renderSentinelTableComponent {Sentinels = sentinels})}
            </body>
            </html>
            """
        let parser = buildHtmlTemplateParser<MainPageProps> template 

        return parser props
    }

In the above code, we:

  • Define props for the page to take in - here just a SentinelCount supplied by the Giraffe endpoint
  • Define a function to render the MainPage
    • We gather data from our Sentinel domain (read more about how this is built: Getting Started with F# and Entity Framework)
    • We also send a command to create a new Sentinel so that you can see this code is live / we have data to show
    • We build the template with a call to our SentinelTableComponent renderer
    • We parse and render the entire template

Rendering HTML with Giraffe endpoints

At this point, we should have a decent idea of how we can build Component-like renderers with type-safe functions. All that's left to do is hook up our Giraffe endpoints to actually render this page.

Explaining the Giraffe web framework and how endpoint routing works is beyond the scope of this article so we won't go into detail. If you want to learn more, check out:

Basically what we've got to do to return this HTML via Giraffe is:

  • Register endpoints for Giraffe to our renderers
  • Tell Giraffe it needs to return our html strings as html (not like a json payload)
  • Create an HTTPHandler for our main page

This seems like a lot and I'll be honest it is a bit of a faff to get fully setup with Giraffe routing. But this approach is very flexible when you inevitably need middleware and for the most part can be handled simply once you've built a helper function.

We're going to build the above steps backwards so we can see how everything is defined and flows into the next.

First we build an HTTPHandler for our main page. This is a type registered by Giraffe that takes in the request context and the next function in the call stack (useful for building composable middleware).

let mainPageHttpHandler 
    (sentinelServiceTree : SentinelServiceTree) 
    = 
        fun (next : HttpFunc) (ctx : HttpContext) -> 
                async {
                    return! 
                        renderMainPageAsync
                            sentinelServiceTree
                            {
                                SentinelCount = 10
                            }
                }

In the above code we:

  • Define our HTTPHandler
  • Call our render main page function, passing down props and dependencies

Next we're going to tell Giraffe that it needs to return the string we give it as html (not a raw string!). We're going to build a helper function because we don't want to have to write this every time. This helper function is basically a composable middleware that is acting on the request / return values - converting the returned string into an html result Giraffe understands.

let renderView 
    (handler : HttpFunc -> HttpContext -> Async<string>)
    : HttpHandler 
    =
    fun(next : HttpFunc) (ctx : HttpContext) ->
        task {
            let! result = handler next ctx
            return! htmlString result next ctx
        }

In this code, we:

  • Define another HTTPHandler
  • Calls its inner handler
  • Convert the inner handler result into something Giraffe recognizes as html via htmlString

Now that we've got that handled, we just need to register our endpoint so Giraffe knows to send requests from this url to this handler. Hopefully this will make it clear how all these utilities work together.

let constructEndpoints (serviceTree : SentinelServiceTree) = 
    [
        GET [
            route "/sentinels" (
                renderView (
                    MainPageView.mainPageHttpHandler serviceTree
                )
            )
        ] 
    ]

Here we:

  • create function constructEndpoints which will create a list of endpoints that the root of your app can then use to register with Giraffe
  • Define a GET route for /sentinels which calls our renderView function for the results of our mainPageHttPhandler

Next

There you have it - server-side rendered HTML with F# / Giraffe, using simple F# functions giving us all the ergonomics and type-safety of the language. I'll be exploring this more on my journey to experiment with reactive server-side UI using tech like HTMX so lmk if you have any Qs and I'll try to answer them next time.

If you liked this post, you might like:

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.