On the Virtue of Boilerplate in Web Languages

On The Virtue of Boilerplate in Web Languages

Jul 10, 2024


HTML + CSS + JavaScript rendered in the browser is by far the best UI infrastructure 1. It runs on every machine through a browser. There are countless resources available online to answer any question you have about programming UI for the web. LLMs can write the entire thing for you. It has never been a better time to write tiny personal apps that leverage the web as an interface.

There’s just one problem: what if you want persistent data? Unless you’re comfortable sticking with local browser storage (I’m not), you will need a database and a backend. To access a database in the browser, your client must communicate with a web server. And servers are complicated 2.

Case study

I was interested in writing a simple application for tracking my finances. I figured it would be easiest to write a terminal-based UI so that I would avoid all that nasty server business. I would run a program from the terminal, as god intended! I tried writing my app using the curses library in Python, but I quickly became frustrated when I had to write a selector dropdown by hand using the low-level rendering interface of curses. I realized I was not interested in reinventing the wheel here. All my interface needs were solved problems. Select2’s richness taunted me in my lowly text-based interface.

Knowing I wanted to use HTML for my frontend and that this required a server, I started thinking about all the web stacks I could use 3. While Python with Flask or Django would certainly be the most practical, I decided to treat this as a practical exercise for learning a new language and its web stack. I considered TypeScript, Elixir, Elm, ReasonML, even Racket, Haskell, and Rust. TypeScript felt too mainstream and I have a strong aversion to Node; Elixir too corporate (the books aren’t free!); Elm and ReasonML too frontend; Racket, Haskell, and Rust not optimized for the web. I heard about Gleam a few weeks ago. It is simple, modern, with a great package manager and an active and friendly community. I downloaded the language and followed a short tutorial on building web apps using Gleam’s web frameworks Wisp and Lustre for back-end and front-end respectively, and Mist for the web server. Here’s what I gleaned.

The structure of a web app

When writing a web application, one can use a familiar structure 4:

This is pretty complicated! Note that almost all of it is essentially reusable boilerplate. I’m sure that very large, complex, and optimized web applications must be written bespoke for performance reasons. But for a simple web application as most people are creating, this is far too high a barrier to using the browser as a rendering engine. Remember that the alternative is writing programs that run entirely in the terminal and print text to the screen—the leap from that to using HTML is far too great. I believe most of this complexity can be hidden from the application developer. And if the developer wants to tweak some of the request handler middleware (how the request is processed), a simple configuration file will do.

If we cut away all of the code that is only there to set up the server, we are left with a reasonably-sized chunk of core application logic:

In my dream language, a web app would look something like this:

import server (serve, respond, Request)

table t (A: string, B: int, C: bool)
  primary key A
  
val cfg = {
  dbpath = "database.db",
  dbms = Sqlite,
  prefix = "MyApp",
  (* ...etc... *)
}

fun layout body =
  <html>
    <head>
      (* ... boilerplate ... *)
    </head>
    <body>
      {body}
    </body>
  </html>

fun home_handler request =
  (* ... *)

fun foo_bar_handler request =
  (* ... *)
  
fun baz_handler request maybeId =
  (* ... *)
  match maybeId
    | Some id ->
        x <- query1 (SELECT * FROM t WHERE t.A = {id});
        ys <- query (SELECT * FROM t WHERE t.B < {x.B});
        respond <ul>{mapX (fn y => <li>{y.B}</li>) ys}</ul>
    | None -> (* ... *)

fun not_found request =
  respond <h1>Not Found</h1>
  
fun router request =
  match request.path
    | "home" -> home_handler request
    | "foo" -> foo_handler request
    | "foo", "bar" -> foo_bar_handler request
    | "baz" -> baz_handler request None
    | "baz", id -> baz_handler request (Some id)
    | _ -> not_found request

fun main () =
  serve cfg router 8080

This looks a little barebones. In particular, there’s no explicit database connection or application state (though we don’t use the latter in this example). I imagine there are monads threading everything (database connections, application state, etc.). That serve function initializes a monad using the data in cfg and then passes that to router, which is monadic. I am eliding function signatures here, but this is a language with strong static types.

Would it surprise you to hear that this language exists and I use it in my day job?

Introducing Ur/Web

Okay, it’s not quite what I described above. Ur/Web is slightly more opinionated than my language, and my syntax is quite fanciful and probably unparseable. But Ur/Web agrees on the key points:

Ur/Web assumes that the functions in your main module correspond to endpoints and automatically launches an Apache server for your application. This is a bit archaic. I think we should specify the endpoints explicitly, and while we’re at it, we might as well require the user put one line of boilerplate to initialize the server using a library function.

The canonical way to define dynamic behavior in Ur/Web revolves around these endpoints: each function corresponds to a URL and its arguments are serialized and passed in the URL as parameters. This is fairly reasonable, but I don’t love it. While Ur/Web has ways around this, I don’t think it’s a great default. User interaction—particularly interacting with a database behind the scenes—should be live and reactive by default. React and other frameworks have popularized this idea. Redirecting the user to a new URL and back every time they query the server is pretty cumbersome. In fact, Ur/Web prefers this model too with its source and signal monads and its dyn and active code blocks in HTML. In my imagined language, reactivity is confidently the default: dynamic components on pages may make remote procedure calls to update data sources, at which point backend listeners automatically re-render the relevant UI whether or not the DOM element which initiated the update is the same as the one receiving it.

Why Not Ur/Web

If Ur/Web is so great, why doesn’t everyone use it? There’s a handful of reasons:

  • Popularity. Ur/Web is Adam Chlipala’s brainchild and it has not enjoyed the marketing and engineering resources that major languages depend on to thrive. Language popularity is a virtuous cycle: languages that people use get more resources devoted to developing and popularizing them. Ur/Web has not entered this positive feedback loop, and so it hangs in obscurity and it lags at the pace of development resultant of a few engineers devoting a small amount of their time to it.

  • Libraries. Ur/Web is under development. Its libraries are not yet stable and it is missing a lot of essential libraries that make a well-rounded programming language. Also, due to the aforementioned popularity problem, practically nobody is developing new libraries for Ur/Web 5.

  • Overly advanced type system. Part of the reason I chose Gleam for my little web app case study is its simple but functional type system. Ur/Web, on the other hand, is an academic language with a really cool type system that also happens to be incredibly difficult to use. Also, like other statically typed functional languages, most of the errors you get in Ur/Web are compiler type-checking errors; unlike other languages, its error messages are obscure 6.

  • Paradigm. Functional languages are becoming increasingly mainstream, but Ur/Web’s corner of advanced ML-style static functional programming with row types is quite an obscure niche within FP. Adopters are unlikely to be familiar with many of Ur/Web’s core concepts unless they are experienced OCaml programmers or have a PhD in programming languages.

  • Outdated architecture. Ur/Web has a strong opinion about the architecture of web apps, and this design has become obsolete relative to the fast pace of web dev state of the art. For example, Ur/Web elides the frontend/backend distinction and instead defers to the compiler to decide what gets run where. On a team split into backend and frontend programmers, this obviously will not fly. The language’s SQL embedding, server setup, and the aforementioned routing are elements of its larger philosophy. If you disagree with this philosophy or find your application going in a different direction, Ur/Web is difficult to justify. The boilerplate of languages less optimized for the web is tolerable.

The Cost of Boilerplate

We can roughly divide the pieces required to implement that Gleam web app into three categories:

  1. One-off server setup boilerplate
  2. Backend & frontend logic
  3. Boilerplate necessary for each additional page, feature, model, etc.

#1 can be hidden or explicit as a matter of taste. #2 is necessary. #3 is what bothers me.

Whenever we add an additional page with a new data type or table underlying it, we have to touch every layer of abstraction from the route parsing to the data encoding and decoding. Ur/Web removes the need for this, since routes are implicit in function and module names and SQL schemas live in the program rather than outside it; you can write this behavior declaratively and the compiler handles the details. Furthermore, the frontend data model never disagrees with the backend because they share the same types 7.

I suggest that much of the apparent complexity of web apps comes from #3. A different approach—like the more principled approach of Ur/Web—avoids it entirely at the cost of flexibility.

But what is the cost of this boilerplate? Actually very little. The one-off server setup is a constant complexity burden on the program, while the data marshalling and other busy work of adding new features carries linear complexity. Neither of these are very bad 8.

Web Stack of Dreams

I must backpedal on my indictment of Ur/Web for fear that Adam will read this and think me unhappy. In fact, I called Ur/Web’s approach principled. This is not a compliment but a descriptor. Principled is exactly what HTML + CSS + JavaScript are not. Ur/Web is out of place among the web languages. Yet it represents a noble vision and perhaps a necessary one. Ur/Web insists that sense can be made of the web stack. It is a kind of beacon or prophecy for what a future full-stack web DSL might look like: a harmony of database, frontend, and backend code, with all the nasty server stuff hidden away in the runtime.

Ur/Web’s philosophy ironically has the potential to be a very accessible way to write web apps for new programmers. By eliding much of the boilerplate that might confuse a newcomer and instead focusing on the core logic, Ur/Web could be a triumph in this niche if not for its experts-only type system.

For now, we trudge along with the unprincipled but practical technologies for web development. Perhaps—dare I dream—Ur/Web will influence the next generation of web stacks.


  1. You may disagree on the fine points. For instance, HTML + CSS + JavaScript is a very tolerant and flexible system because it has to work well enough on all browsers on all systems since the web was invented. HTML doesn’t have to be well-formed like XML. JavaScript is dynamic and has all kind of weird behavior by design. It is reasonable to critique this system, but only if you are honest about its intended use cases. If you are developing a big, complex video game with realistic graphics, then maybe the web isn’t the right interface. Then don’t say JavaScript is bad; say JavaScript is not the tool for the job. “But,” you say, “everything’s on the web these days! I have to use JavaScript and HTML and CSS!” No you don’t! Transpilers are a thing! There’s TypeScript, js_of_ocaml, ReasonML, Elm, and probably a whole bunch of other popular ones I haven’t heard of because they’re imperative and I typically prefer functional. You can use the flexible interface of the web but perform static checking and avoid most of the common frustrations of the frontend stack.↩︎

  2. Even totally local web apps are complicated! Browser security limits the user’s ability to interact with the world outside the browser except through secure protocols like HTTPS. This means no dynamic pages loading data from the local filesystem. It is technically possible to write a fully local web app—i.e., frontend only—with a database by dropping the database file in through a form or event listener and querying it with something like sql.js… but my contention is that writing server-backed apps should be easier and more accessible.↩︎

  3. At this time I should note that I’m technically a professional web developer. I use Ur/Web at my job. More on that later. I am not very familiar with the normie web stacks like full-stack JavaScript of TypeScript using frameworks like React, let alone any of the object-oriented stacks. I’ve messed around with Flask and Django before, but I never got into it, and while I spent a summer learning React, I do not remember any of it. Truth be told, my experience with web servers is a programming assignment from sophomore year where we had to write a toy web server from scratch in C using the Unix sockets API. A great test of C skills to be sure, but not a good way to quickly build basic web applications. So yeah, this is my first time writing a web app for realsies. But how can I be a backend web developer if I don’t know how servers work? You will see.↩︎

  4. Big disclaimer: I am learning this for the first time. To my understanding, there are many diverse ways to structure web applications, and for each one there are countless frameworks that provide diverse interfaces to such a system. Here, for most of the points in this article, I am considering a web app that uses minimal frameworks for frontend and backend and writes the routing logic manually, making use of libraries for stuff like request parsing and the actual server code.↩︎

  5. Except me and a handful of my friends.↩︎

  6. And I say that as someone who is comfortable with Haskell and OCaml type errors! The situation must be dire indeed!↩︎

  7. Happily, Gleam and other full-stack languages that compile to JavaScript share this property. TypeScript is especially similar to Ur/Web in this sense, especially when used with frameworks like React.↩︎

  8. At least we don’t have a quadratic burden where each new component needs to define its relationship to each other one! I cannot think of a case where that might happen in web apps. Perhaps they are simple by nature. Or perhaps decades of web development have discovered the best practices for how to structure web apps in a relatively hygienic fashion. Either way, I learned this structure for the first time yesterday by reading the tutorial and it did not take long to understand and appreciate.↩︎