Essay - Published: 2024.10.08 | create | fsharp | fullstack | giraffe | giraffe-viewengine | tech | webapp |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
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.
In this post we'll be creating a minimal fullstack web app with F# + Giraffe.
The web app has 3 routes:
/ - Shows all items/detail/ID - Shows the item with matching id (or NOT FOUND)/create - Creates a new itemIn 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.
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.
The index page / returns a count and list of all items in our data repository.
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 ]
[]
}
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.
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 ]
[]
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 -> createHttpHandlerFor 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
}
)
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:
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.