Good ol' Monoliths

In an era where everything is a SPA and everything uses Webpack, it seems that you should only be doing web development as a SPA, cramming a ton of javascript on your website. Monoliths or also called MPA's (Multi-Page Applications) for some reason are almost always ruled out. I'm not here to convince you but, I think that's not always the best approach and you shouldn't rule out things for the sake of ruling them out.

This will be a series where I expose some of my experiments with F#

Today I'll be telling you about a website I made with Giraffe, Razor Pages, and ES modules javascript. No webpack, no bundlers, no weird setups, just Server Side Rendered HTML and a few lines of javascript sprinkled on top of that

{% github AngelMunoz/Somnifero %}

the code I'll be showing is under the webrtcchat branch

That repo contains a few experiments I'll be posting in the following weeks I'll name some.

  • Multi-Page Application (this post)
  • ES Modules Scripts for SSR'd apps (this post)
  • Realtime Communication with Signalr
  • WebRTC Video Broadcasting
  • docker-compose setup
  • MongoDB as a database for an F# backend

the file structure is the following one Project Structure it has a bunch of docker files and files and folders that might make it a little bit messy but it isn't that messy at all

WebRoot is basically where the static files for the server reside (any images, javascript, css, etc)

Views is where the HTML that uses Razor Pages resides

The rest is either a docker file or F# code, the F# solution is quite simple actually

F# Code Actual Structure

One thing to remember is that F# requires File ordering, everything related to F# code cascades from top to bottom, so in this case Types.fs has the most accessible code in the whole application where Program.fs code is only accessible within itself. Let's review what kind of routes we have here

let webApp =
    choose [ GET >=> route "/" >=> Public.Index
             route "/auth/signout"
             >=> signOut CookieAuthenticationDefaults.AuthenticationScheme
             >=> redirectTo false "/"
             GET >=> route "/broadcast" >=> Public.Broadcast
             GET >=> route "/watch-broadcast" >=> Public.Watch
             POST
             >=> (choose [ route "/auth/login"
                           >=> validateAntiforgeryToken Public.InvalidCSRFToken
                           >=> Auth.Login
                           route "/auth/exists" >=> Auth.CheckExists
                           route "/auth/signup"
                           >=> validateAntiforgeryToken Public.InvalidCSRFToken
                           >=> Auth.Signup ])
             subRoute
                 "/portal"
                 (requiresAuthentication (challenge CookieAuthenticationDefaults.AuthenticationScheme)
                  >=> choose [ GET >=> route "/home" >=> Portal.Index
                               GET >=> route "/me" >=> Portal.Me ])

             subRoute
                 "/api"
                 (requiresAuthentication (challenge CookieAuthenticationDefaults.AuthenticationScheme)
                  >=> choose [ GET >=> route "/me" >=> Api.Me ])

             setStatusCode 404 >=> text "Not Found" ]

Note: the >=> combinator means "compose". You can read more here

This means that there are basically 10 routes that range from a simple request Html rendering calls to RestAPI endpoints and so on as you can see in that router, we have Cookie and AntiForgery checks as security measures. Let's start with our Index route

module Public =
    // this is a small helper to create a tuple with a key and a boxed value
    let inline (+>) (a: string) (b: 'T): string * obj = a, box b

    let Index =

        fun (next: HttpFunc) (ctx: HttpContext) ->
            task {
                let data =
                    dict<string, obj>
                        [ "Title" +> "Welcome"
                          "HeaderData" +> { routeGroups = Seq.empty }
                          "FooterData"
                          +> { routeGroups = Seq.empty
                               extraData = None } ]
                    |> Some

                if ctx.User.Identity.IsAuthenticated
                then return! redirectTo false "/portal/home" next ctx
                else return! razorHtmlView "Index" None data None next ctx
            }

Index Home

The index route is probably the simplest one, we just check if the user is authenticated, if the user is authenticated we do a redirect (since the index is our login page as well) and if not we return a razorHtmlView which will try to look for the file Index.cshtml we don't pass a model, but we pass some ViewData.

If you have used any kind of templating engine before, like jinja, ejs, jade (formerly pug), handlebars the following contents won't be much different, the Index file uses Giraffe.Razor as the view engine, so if you're an ASP.NET developer that might want to dive a bit into F# this can be an opportunity for you 😉.

@using Somnifero.ViewModels;

@{
    Layout = "_Layout";
    var headerData = ViewData["HeaderData"] as HeaderData;
    var footerData = ViewData["FooterData"] as FooterData;
}
@section Header {
  @await Html.PartialAsync("_Header", headerData)
}
<main class="som-main">...

@section Scripts {
<script src="/js/index.js" type="module"></script>
}

@section Footer {
  @await Html.PartialAsync("_Footer", footerData)
}

you can see razor files use C# hence why I mentioned if you are a C# looking into F# territory you can go step by step instead of replacing C# from the get-go

I've omitted most of the HTML contents since they are the HTML you know and love there's nothing ultra strange about those. In this case, we just declare which file is our layout and pull the header and footer data from the ViewData dictionary and render those partials inside the Header and Footer accordingly. in our scripts section, we have one of our first niceties of the modern era of javascript an "ES module", even if it took us five years or more to get there and perhaps with some caveats since there might be users that yet don't have an advanced enough browser we can use today ES modules as part of our javascript files in the browser without transpiling them.

// YES FINALLY just pull your library and import stuff from it
import { enhance } from 'https://unpkg.com/aurelia-script@1.5.2/dist/aurelia.esm.min.js'

class LoginFormViewModel {}

class SignupViewModel {}

enhance({
  host: document.querySelector('.loginsection'),
  root: LoginFormViewModel
});

enhance({
  host: document.querySelector('.signupsection'),
  root: SignupViewModel
});

function getCSRFTokenFromForm(form) {}

Our javascript file is quite small and does what you would expect, make HTML dynamic 😁.

Register

Here we have two classes that are used in conjunction with the aurelia-script library. Basically, we just render our HTML right from the server, add a bit of javascript to it, and continue our lives.

I'll be adding another post for aurelia-script in the future

This page is an example of how you can just pull your HTML from the server and sprinkle some functionality over it with an ES module.

what about if you want something not so static? let's say a chat and you just want to render the initial page then you want to let javascript get full control of it? let's go over the Portal page

module Portal =

    let Index: HttpHandler =
        fun (next: HttpFunc) (ctx: HttpContext) -> task { return! razorHtmlView "Portal/Index" None None None next ctx }

Boom, blazing fast! just render my file already!


@{
    Layout = "../_Layout";
    ViewData["Title"] = "Portal";
    /// these will be empty since we didn't pass any information
    var headerData = ViewData["HeaderData"] as HeaderData;
    var footerData = ViewData["FooterData"] as FooterData;
}
@section Header {
  @await Html.PartialAsync("../Shared/_Header", headerData)
}
<!-- I don't want to have a lot of HTML, just render my stuff -->
<main class="som-main" name="portal"></main>

@section Scripts {
<script src="/js/portal/index.js" type="module"></script>
}

@section Footer {
  @await Html.PartialAsync("../Shared/_Footer", footerData)
}

Simply render stuff

Now instead of just pulling the enhance to lift a small part of the HTML, we'll go full Aurelia and we will be pulling messages with Signalr and rendering a small chat PoC

import { Aurelia, PLATFORM } from 'https://unpkg.com/aurelia-script@1.5.2/dist/aurelia_router.esm.min.js'
const aurelia = new Aurelia();
aurelia
    .use
    .standardConfiguration()
    .developmentLogging();

aurelia.start()
    .then(aur => aur.setRoot(PLATFORM.moduleName("/js/portal/app.js"), document.querySelector("[name=portal]")))
    .catch(error => {
        console.error(`Something went wrong starting the portal page:\n"${error.message}"`);
        UIkit.notification({
            message: 'There was an error starting this page, please reload to avoid data loss',
            status: 'danger',
            pos: 'top-right',
            timeout: 5000
        });
    });

Chat

If you are interested you can check the following files WebRoot/portal

I know there is a LOT going on in that project I'll try to expand on each part of the stack in further posts.

I hope this post kind of sheds light on how using F# is super simple (if you see the route handlers they are like... 50-60 lines big?) and that not everything needs to be a SPA and not everything needs a 2MB bundle, I used aurelia, but you can indeed use react, Vue whatever you like on your frontend stack.

If you liked it feel free to share it, if you have those C# friends that want... but don't want to fully commit to F# let them know they can still use razor pages.

As always you can comment below or ping me on Twitter 😁

Is there something wrong? Raise an issue!
Or if it's simpler, find me in Threads!