Build a Simple Fullstack Web App with F# + Giraffe

Date: 2024-10-08 | create | tech | fsharp | giraffe | webapp | giraffe-viewengine | fullstack |

In my last post we built a web API with F# + Giraffe that handles data and returns string representations of that data via its endpoints.

In this post we'll evolve those endpoints to return HTML instead - effectively turning our minimal web API into a fullstack web app with data, endpoints, and a frontend UI.

F# / Giraffe Web App Overview

In this post we'll be creating a minimal fullstack web app with F# + Giraffe.

  • Frontend: Server-side Rendered HTML with Giraffe.ViewEngine (an HTML DSL) (learn more)
  • Backend: F# / Giraffe (on ASP.NET) (learn more)
  • Data: In-memory Data Repo (learn more)

The web app has 3 routes:

  • / - Shows all items
  • /detail/ID - Shows the item with matching id (or NOT FOUND)
  • /create - Creates a new item

In this post we'll be focusing on how we're rendering our SSR HTML with Giraffe.ViewEngine. For deep dives into other parts of the app, check out the rest of the series:

Want access to this project's full source code? HAMINIONs members get access to this project's full source code (github) as well as dozens of other example projects.

Building Server-Side Rendered Frontends with F# and Giraffe.ViewEngine

Giraffe.ViewEngine is an HTML DSL. This means it builds up a representation of the HTML using F#-native datastructures and operations. This is useful because it allows you to leverage the power of F#'s type system and data operations to build your HTML which often leads to safer, more dynamic generation.

For more on Giraffe.ViewEngine, its capabilities, and why you might want to use it: Server-side HTML Rendering with F# and Giraffe.ViewEngine

The first thing we're going to do is build a base HTML layout for our site's pages. This is a common practice in frontend web apps as it allows us to build site pages faster with more consistency and less boilerplate. This will also give you an idea of how you might build and use your own templates / components in a web app.

Here we're going to use a slots approach to make this layout composable. This will make it more useful in more usecases by giving more power to the caller for how they want it to be built.

renderWithBaseLayout

let renderWithBaseLayout 
    (headSlot: XmlNode list)
    (bodySlot: XmlNode list)
    (footerSlot: XmlNode list)
    : XmlNode
    = 
    html [] [
        head [] [
            meta [
                _charset "UTF-8";
                _name "viewport";
                _content "width=device-width, initial-scale=1"
            ];
            yield! headSlot;
        ]
        body [] [
            yield! bodySlot;
        ]
        footer [] [
            yield! footerSlot;
        ]
    ]

With the base layout taking care of the HTML boilerplate, we can move onto rendering each of our pages.

Rendering the Index Page

The index page / returns a count and list of all items in our data repository.

  • Takes in a list of items
  • Displays the count
  • Displays the list of items

This is an example of how using a DSL allows us to leverage F#'s list operations to dynamically generate our HTML (all in a type-safe way!).

renderIndexPage

let renderIndexPage
    (allItems: SimpleItem list)
    : Task<XmlNode>
    =
    task {
        let count = allItems.Length

        let itemListMarkup = 
            div [] [
                h3 [] [ str $"Total Count: {count}"]
                ul [] [
                    yield! 
                        allItems
                        |> List.map (fun i -> 
                            li [] [ str (i.Id.ToString()) ]
                        )
                ]
            ]
            
        return 
            renderWithBaseLayout
                [] 
                [ itemListMarkup ]
                []
    }

Rendering the Detail Page

The detail page /detail/ID takes in a single item option. We use option here because there's always the possibility the ID is bad (and thus no corresponding item) and it's usually good practice to handle known edge cases.

  • If no item -> Not found!
  • If item -> Display the ID

renderDetailPageMarkup

let renderDetailPageMarkup 
    (item: SimpleItem option)
    : XmlNode 
    =
    let itemMarkup = 
        div [] [
            match item with 
            | None -> h3 [] [ str "Not Found!" ]
            | Some i -> h3 [] [ str $"Id: {i.Id.ToString()}" ]
        ]
     
    renderWithBaseLayout
        [] 
        [ itemMarkup ]
        []

Returning SSR HTML Pages to Giraffe Endpoints

So far we've built up DSL representations of our HTML pages with F# but we haven't yet turned them into HTML or returned it to the caller. Here we'll discuss how we hook up our Giraffe endpoints to these HTML components.

First - this is how we declare our endpoints:

let webApp =
    [ 
        GET [ 
            route "/" (getAllHttpHandler) 
            routef "/detail/%s" (
                fun id -> detailHttpHandler id
            )   
            route "/create" (createHttpHandler)     
        ]
    ]

Basically this is just mapping routes to functions that will fulfill the request:

  • / -> getAllHttpHandler
  • /detail/ID -> detailHttpHandler id
  • /create -> createHttpHandler

For more details on Giraffe Endpoints, check out my other posts:

In each of our handlers, we're transforming Giraffe.ViewEngine's HTML DSL into an HTML string using RenderView.AsString.htmlNode. This gives us a data type that can be written as a response back to the caller using ctx.WriteHtmlStringAsync.

createHttpHandler

let createHttpHandler =
     handleContext( fun (ctx: HttpContext) -> 
        task {

            let newItem = itemRepo.Create()

            let markup = 
                renderWithBaseLayout
                    [] 
                    [ 
                        h3 [] [ str "Item Created!" ]
                        p [] [ str $"New Item: {newItem.Id.ToString()}" ]
                    ]
                    []

            return! 
                markup
                |> RenderView.AsString.htmlNode
                |> ctx.WriteHtmlStringAsync
        }
    )

getAllHttpHandler

let getAllHttpHandler =
    handleContext( fun (ctx: HttpContext) -> 
        task {
            let! indexPageMarkup = 
                renderIndexPage
                    (itemRepo.GetAll())

            return! 
                indexPageMarkup
                |> RenderView.AsString.htmlNode
                |> ctx.WriteHtmlStringAsync
        }
    )

detailHttpHandler

let detailHttpHandler
    (id: string)
    =
    handleContext( fun (ctx: HttpContext) -> 
        task {

            let item = itemRepo.GetOne(id)
            let detailPageMarkup = renderDetailPageMarkup item

            return! 
                detailPageMarkup
                |> RenderView.AsString.htmlNode
                |> ctx.WriteHtmlStringAsync
        }
    )

Next

That's a minimal example of how you might build a fullstack webapp with F# using Giraffe.ViewEngine to build your Server-Side Rendered HTML.

Want to build a fullstack webapp with F#? CloudSeed (my F# project boilerplate) gets you up and running with a fullstack webapp using F# + Giraffe in 10 minutes so you can start working on your app, not fiddling with setup.

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.