{"slug": "javascript-sucks-here-s-how-to-build-with-vite-and-gleam", "title": "JavaScript Sucks. Here's How to Build with Vite and Gleam", "summary": "A developer frustrated with JavaScript's flaws advocates for using Gleam, a type-safe functional language, with Vite as a build tool to create web applications. The combination leverages Gleam's safety and Vite's modern tooling, treating JavaScript as a foreign function interface rather than replacing it.", "body_md": "# JavaScript sucks. Here’s how to build with Vite and Gleam\n\n## TL;DR\n\n**I am not attacking JavaScript**. I only wrote highlights of my personal frustrations with JS and how I mitigated them. Most of the article is about Gleam and Vite and goes over the challenges I faced and the reasoning behind my decisions.\n\n[Here’s the template itself](https://codeberg.org/hendassa100k/vite_gleam_template). You can check the source code and you can begin building.\n\nFor years, I’ve felt this pain: the tooling is great, but the language is fundamentally flawed. JavaScript was never designed properly. I mean *never*. I’ve worried to even touch this language for this exact reason. Web developers spent a decade building increasingly elaborate band-aids to make it work. While TypeScript provides a safety net, it often feels like weak tape holding together a structure that wasn’t built to be sane for developers. I wanted something better that doesn’t change my values.\n\nThat’s when I discovered [Gleam](https://gleam.run/). Gleam is a type-safe, functional programming language. While I didn’t wrote a single program in functional programming language, I realized that’s exactly what a scripting language should be. Gleam offers proper type safety and a functional approach that makes writing robust, safe, and concurrent code - the very things the web requires. Unlike TypeScript, it doesn’t try to wrap shit in gold. It throws all of that out the window, but still leaves you a way to interact with it.\n\nHowever, Gleam was initially built for Erlang. JavaScript support was a later addition, meaning the tools are still maturing. On the other hand, the JavaScript ecosystem for build tools is unparalleled.\n\nBut when I tried to find tools for this and then I stumbled across [Vite](https://vite.dev/). Vite is the ultimate glue. It handles live reloading, bundling, and asset management and all with a massive plugin ecosystem. Because Vite is plugin driven, it doesn’t care what language you are writing in, as long as it can eventually turn it into something the browser understands.\n\nBy combining Gleam’s type-safe logic with Vite’s build pipeline, we can get the best of both worlds: The safety of functional programming with the speed of modern web tooling.\n\nYou propably saying “Dude, just use WASM”. WASM is powerful, yes, but it isn’t good for web development *at least for now*. It currently faces significant issues, including [limited DOM access](https://queue.acm.org/detail.cfm?id=3746174) and issues with startup times due how it is designed. Even if WASM were perfect, we still need a native scripting language to interact with the browser. JavaScript is like the “C” of the web: it is the standard, the most adopted, and the most compatible way to talk to the DOM. C and JavaScript are going to outlive the human race and it propably will never change.\n\nSpeaking of C this is why Gleam’s approach is so smart. Gleam treats JavaScript as a FFI. It doesn’t try to replace the browser’s native language; instead, it provides a way to interact with it safely. Gleam forces you to write type-safe code, protecting you from the JavaScript traps like `null`\n\nand unpredictable exceptions or even dynamic typing for god’s sake, while still letting you use JS when you need to.\n\n# Technical Deep Dive\n\n## Understanding Gleam FFI\n\nTo build a production-ready app, you need to understand how Gleam interacts with the JavaScript runtime.\n\nOne of the main goals of Gleam is being multilingual. Gleam uses FFI to call JavaScript functions for that. You define the external function in your Gleam code and provide the path to the JS implementation:\n\n```\n@external(javascript, \"./assets.ffi.js\", \"get_asset\")\npub fn load_asset(url: String) -> Promise(AssetResult)\n```\n\nAnd Gleam’s strongest features is that it does not support JS types directly. You cannot simply “import” a dynamic JS object into Gleam and expect it to stay safe. Instead, Gleam forces you to define a type that represents the data.\n\nFor example, you can create a type:\n\n```\n// This type has no constructors, so it cannot be initialized directly.\n// It acts as a \"handle\" for the JS value.\npub type DateTime\n\n@external(javascript, \"./js.ffi.mjs\", \"now\")\npub fn now() -> DateTime\n```\n\nBy doing this, Gleam ensures that the rest of your application treats object as a type-safe citizen, while the actual messy work happens behind the FFI boundary.\n\nSince JavaScript doesn’t have a native Result type (it uses exceptions) or Gleams types in general, Gleam provides a `gleam.mjs`\n\nmodule. This allows to map JS try/catch blocks into Gleam’s Result types for example:\n\n``` js\nimport { Result$Ok, Result$Error } from './gleam.mjs';\n\nexport function get_foo_asset(img) {\n  try {\n    let result = get_asset(\"foo.png\");\n    return Result$Ok(result);\n  } catch (err) {\n    return Result$Error(AssetError$AssetNotFound);\n  }\n}\n```\n\n## Building our first app with Lustre\n\nWith that in mind, let’s build our first application. For this, I’ve chosen [Lustre](https://lustre.hexdocs.pm/index.html).\n\nLustre is a mature framework that follows the Elm Architecture, which feel familiar to React developers. Even though I hate the Virtual DOM, there is a good reason to use it: it is significantly safer than janky query selectors because every element in your UI is strictly tied to your application’s state and logic.\n\nFrom the official documentation:\n\nThere are three main building blocks to the Model-View-Update architecture:\n\nA\n\n`Model`\n\nthat represents your application’s state and an`init`\n\nfunction to create it.A\n\n`Message`\n\ntype that represents all the different ways the outside world can communicate with your application and an`update`\n\nfunction that modifies your`model`\n\nin response to those messages.A view function that renders your\n\n`model`\n\nto HTML, represented as an`Element`\n\n.\n\nHowever, real apps need Side Effects (like making HTTP requests). In Lustre, you handle these by returning an `Effect`\n\nfrom your `init`\n\nor `update`\n\nfunctions. These effects allow you to step outside the pure logic to perform a concurrent action and then send a message back into the application loop. I’ll not go into detail here, so check out the [official documentation](https://lustre.hexdocs.pm/lustre.html) and [effects](https://lustre.hexdocs.pm/lustre/effect.html).\n\nThe biggest pain you’ll face is that there isn’t a trivial way to handle JavaScript Promises in Gleam. While the `gleam_javascript`\n\nlibrary provides FFI for various JS types some concurrent stuff requires a way to bridge Gleam’s types into JS ones.\n\nWe can solve this using `lustre.effect`\n\n. When you initialize your app with `lustre.application`\n\n, the `init`\n\nfunction returns a tuple: your initial `Model`\n\nand an `Effect`\n\n.\n\n``` php\nfn asset_effect(name: String, path: String) -> effect.Effect(Message) {\n  effect.from(fn(dispatch) {\n    assets.load_asset(path)\n      |> promise.map(fn(response) { AssetLoaded(name, response) })\n      |> promise.tap(dispatch)\n\n    Nil\n  })\n}\n\nfn init(_) -> #(Model, effect.Effect(Message)) {\n  #(\n    Model(0, dict.new()),\n    effect.batch([asset_effect(\"logo\", \"/src/assets/lucy.svg\")]),\n  )\n}\n```\n\nWe need to convert the result of our promise into a `Message`\n\nthat Lustre’s application loop understands then we need to use `promise.map`\n\nto transform that result into an `AssetLoaded`\n\nmessage and call `promise.tap`\n\n, which executes the dispatch once the promise resolves.\n\nYou might wonder why we return `Nil`\n\nat the end. Gleam has no `return`\n\noperator, so it returns last expression. In our case `promise.tap`\n\nreturns our current Promise. But we don’t care about it, so we just ignore result value and result `Nil`\n\n.\n\nNow we have proper assers with lazy import using Vite, when it loads it will be avaliable once `AssetLoaded`\n\narrives and we can store in Dictionary for example. Without it we would import all assets all at once, but it’s not what you propably want.\n\n# Conclusion\n\nWe’ve seen how moving away from the JS toward something more decent. Gleam supported by the powerhouse that is Vite, which can result in a much more sane development experience.\n\nI’ve shared the source code for [my template here](https://codeberg.org/hendassa100k/vite_gleam_template). I didn’t cover every single possible edge case in this article; instead, I focused on the hardest points I faced during development. For everything else, the documentation is excellent and the patterns are self-explanatory.\n\nI spent a lot of time reading source code to reach the conclusions I shared today. I believe that’s the best way to learn, and I’ve provided this template to make that journey a little easier for you. Now, go build something better.", "url": "https://wpnews.pro/news/javascript-sucks-here-s-how-to-build-with-vite-and-gleam", "canonical_source": "https://hendassa100k.github.io/posts/2026-07-11-gleam-and-vite/", "published_at": "2026-07-17 08:40:25+00:00", "updated_at": "2026-07-17 08:51:36.427987+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Gleam", "Vite", "JavaScript", "TypeScript", "Erlang", "WASM"], "alternates": {"html": "https://wpnews.pro/news/javascript-sucks-here-s-how-to-build-with-vite-and-gleam", "markdown": "https://wpnews.pro/news/javascript-sucks-here-s-how-to-build-with-vite-and-gleam.md", "text": "https://wpnews.pro/news/javascript-sucks-here-s-how-to-build-with-vite-and-gleam.txt", "jsonld": "https://wpnews.pro/news/javascript-sucks-here-s-how-to-build-with-vite-and-gleam.jsonld"}}