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:
- A directory containing databases;
- A directory containing front-end assets like CSS and JavaScript;
- A main entrypoint module that initializes the web server and passes all requests to the router;
- A module containing the boilerplate for processing a request, preparing a response, and setting up the internal state of the server;
- A router module that uses the request handler to process the route and dispatch to various functions for performing server-side computations and generating HTML pages in response;
- A module or set of modules for the layout and rendering of the pages;
- A module containing models of the application data and server-side transformations thereof;
- A module containing the server-side computations, database queries, etc. that are called by the router.
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:
- Database
- Front-end assets
- Router
- Layout/rendering
- Data model
- Server-side computations
In my dream language, a web app would look something like this:
import server (serve, respond, Request)
string, B: int, C: bool)
table t (A:
primary key A
val cfg = {
"database.db",
dbpath =
dbms = Sqlite,"MyApp",
prefix = (* ...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 () =
8080 serve cfg router
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:
- Static types
- Functional paradigm
- Embedded SQL and HTML with static types checking well-formedness
- Syntax for defining tables
- Designed specifically for the web
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:
- One-off server setup boilerplate
- Backend & frontend logic
- 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.
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.âŠď¸
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.âŠď¸
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.âŠď¸
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.âŠď¸
And I say that as someone who is comfortable with Haskell and OCaml type errors! The situation must be dire indeed!âŠď¸
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.âŠď¸
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.âŠď¸