Build a Fullstack Webapp with F# + Falco
Date: 2024-11-06 | create | tech | fsharp | falco | webapp |
In my last post we built a single-file web api using F# and Falco.
In this post we'll turn that API in to a fullstack webapp by returning HTML from our endpoints, using Falco.Markup as an HTML DSL.
App Overview
The app has three endpoints:
/
- Gets all items/detail/{id}
- Gets a single item by id (if exists)/create
- Creates a new item
This is the same url format as the API we built previously and uses the same internal data repo.
Note: HAMINIONs members get access to the full project files (github) so you can download and run this yourself.
Building HTML with Falco.Markup
The main difference from the previous web api is that we're returning HTML from our endpoints. To build this HTML we're using Falco.Markup as an HTML DSL. I like HTML DSLs because they allow us to build our markup with F#-native techniques giving us type-safety, composability, and access to all the data operations we're used to.
Falco.Markup's API is very similar to that of Giraffe.ViewEngine (which I currently use in many projects):
- Build up nested lists of XMLNodes that can be compiled into html strings
- Each node typically has 3 components:
- Name / type of the Node
- List of attributes (like
class
orhx-trigger
) - List of child nodes
Together this allows us to declare our HTML using F# native types which is pretty cool!
Returning HTML from our endpoints
Now that we have an understanding of Falco.Markup and how it can be used to represent HTML, we'll use it to build our webpages.
Here we'll be returning HTML from our /
and /detail
routes.
Endpoints:
let webAppEndpoints =
[
get "/" getAllHttpHandler
get "/detail/{id}" (detailHttpHandler)
get "/create" createHttpHandler
]
First we'll create a base layout to reuse for our webpages. I like this approach because often many pages will want to reuse the same things like assets (scripts / styles) and settings so declaring this in one place makes keeping it uniform easier.
renderWithBaseLayout
creates a base HTML template while allowing the caller to pass in additional items for each section (head, body, and footer).
let renderWithBaseLayout
(headSlot: XmlNode list)
(bodySlot: XmlNode list)
(footerSlot: XmlNode list)
=
Elem.html [] [
Elem.head [] [
Elem.meta [
Attr.charset "UTF-8"
Attr.name "viewport"
Attr.content "width=device-width, initial-scale=1"
]
yield! headSlot
]
Elem.body [] bodySlot
Elem.footer [] footerSlot
]
For the index route (/
) we create a div with a header and unordered list of items:
let getAllHttpHandler: HttpHandler =
fun (ctx: HttpContext) ->
task {
let count = itemRepo.GetAll().Length
let itemListMarkup =
Elem.div [] [
Elem.h3 [] [ Text.raw $"ItemCount: {count}" ]
Elem.ul [] [
yield!
itemRepo.GetAll()
|> List.map (fun item ->
Elem.li [] [ Text.raw (item.Id.ToString()) ]
)
]
]
return
Response.ofHtml
(
renderWithBaseLayout
[]
[itemListMarkup]
[]
)
ctx
}
For the detail page (/detail
) we return different text depending on if the item was found or not:
let detailHttpHandler: HttpHandler =
fun (ctx: HttpContext) ->
task {
let route = Request.getRoute ctx
let idParam = route.GetString "id"
let item = itemRepo.GetOne(idParam)
let itemMarkup =
Elem.div [] [
Elem.h3 [] [
match item with
| None -> Text.raw "Not Found!"
| Some i -> Text.raw $"Found: {i.Id}"
]
]
return
Response.ofHtml
(
renderWithBaseLayout
[]
[itemMarkup]
[]
)
ctx
}
Next
Overall I've enjoyed building with Falco and I'm glad to see there's a markup DSL that plays nice with it. Theoretically I think you can use Falco.Markup and Giraffe.ViewEngine interchangeably because both are just XMLNode lists that can be rendered to html strings but I haven't tried it yet.
Thanks you to HAMINIONs members for your support - you make these guides possible! HAMINIONs have access to the full project files for this guide (github) as well as dozens of other example projects.
If you liked this post please let me know by liking and subscribing and telling me what else you'd like to see me cover.
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.