The misguided dream of language monogamy in web development
In the ever-changing world of web development, many seek the ideal of using one language for both frontend and backend. But if we focus on common data instead of a common language, we can get the best out of both worlds without having to sacrifice significant synergies. —
Content:
- Exploring the One-Language Dream
- Unraveling the Illusion
- Shared Data Definition vs. Language Uniformity
- A Real-world Example: Rust and Elm Collaboration
Exploring the One-Language Dream:
Many are drawn to the idea of sticking to a single programming language for both frontend and backend.
Node.js is a runtime environment that allows developers to execute JavaScript code server-side
One reason is the appeal of having to master just a single language For example NodeJS offers JavaScript developers to write their backend in the same language that the browser understands.
htmx is a library that allows you to access modern browser features directly from HTML, rather than using javascript.
htmx advertises more to backend developers by having to only produce HTML in their backend language to create a dynamic UX.
A second argument, is the idea of writing certain functionalities only once and being able to use them both on front- and backend. However, let’s pause and consider whether these reasons paint the complete picture or merely optimize for a local maximum.
Unraveling the Illusion
I would argue that the surface area of code/functionality that can genuinely be reused between the frontend and the backend is often overestimated. The backend and the browser, operate in vastly different runtime environments, demanding distinct programming patterns. In the browser, we run an interactive, graphical user interface. Our primary goal in the backend is the correct and efficient management of business data.
I would further argue that in most cases you will spend at least as much (if not more) effort learning the framework (and the problem they’re solving) as you spend learning the programming language.
The reason that there are little synergies between learning let’s say React and Ruby on Rails is not so much that they are written in different languages. It’s the fact that they solve completely disparate problems.
Shared Data Definition vs. Language Uniformity
The main potential for code reuse is the use of shared data structures!
GraphQL
lets you define a schema for the types and relationships of data in an API, serving as a contract between the client and the server, ensuring structured and consistent communication.
To achieve this, many companies use GraphQL. However, managing a “GraphQL layer” is associated with considerable costs. Companies often have entire teams dedicated solely to the integrity of the GraphQL API. On top of that GraphQL comes with its own (schema/query) language that developers have to learn as well.
If you focus on the right problem - “the common data definition” - and put the challenge of learning a new language in the right perspective, a lot of interesting possibilities open up. Rather than fixating on the homogeneity of programming languages, focusing on a shared understanding of data structures provides a more pragmatic and effective approach to achieving code reuse where it matters the most.
A Real-world Example: Rust and Elm Collaboration
Elm is a small programming language focused on simplicity. In my previous post, I go into detail why I think everyone should give it a try.
Elm is a functional programming language for front-end web development, known for its strong type system and focus on simplicity, correctness, and reliability in building interactive user interfaces.
Rust excels in the backend with its focus on safety, performance, and concurrency, offering a robust and efficient development experience.
Choosing Elm as the frontend language and Rust in the backend gives us the best of both worlds. Since both are statically typed we can use the type information from our Rust data structures to generate Elm types and (de-)serializers.
elm_rs is a Rust crate that automatically generates type definitions and functions for your Elm frontend from your Rust backend types, making it easy to keep the two in sync.
The backend makes the natural candidate for the “source of truth” of the shared
data type. The elm_rs
crate translates the Rust data types into the
corresponding Elm types (and de-/serializers).
Instead of adding complexity through a dedicated API layer we define
two data types ToBackend
or ToFrontend
, each representing messages flowing
either to the backend or to the frontend.
This way of communication is highly inspired by Lamdera.
Lamdera is a full-stack platform that actually does use Elm in both the frontend and the backend.
But it also imposes significant restrictions on how you can run code in the backend.
In this way, we can use Elm for what it was created for: Working with the Brwoser API in a safe and delightful way without having to sacrifice a responsive UI. In return, we don’t have to deal with HTML and the state of the user interface in the backend at all.
My starter template “Rülm” shows that such an architecture beyond the popular web frameworks
does not have to be complicated. In less than 400
lines of code we get a web application that is type-safe
and efficiently exchange data in both directions.
And all this without external build tools!