Gleam compilado a JavaScript: usando Gleam en el frontend y con Node.js

Gleam es uno de los pocos lenguajes que puede compilar a dos targets completamente diferentes: Erlang (para correr en la BEAM) y JavaScript (para Node.js, Deno o el navegador). Esto no es algo que se haya añadido como experimento: el soporte JavaScript es un ciudadano de primera clase en el compilador y ha mejorado notablemente con cada release, siendo Gleam 1.7 (enero 2026) la versión con mejor interop JS hasta la fecha.

Cómo funciona el target JavaScript

Para compilar tu proyecto a JavaScript en lugar de Erlang, basta con especificar el target en el comando de build:

gleam build --target javascript
gleam run --target javascript
gleam test --target javascript

El compilador genera módulos ESM (ES Modules) estándar. Esto significa que el output es JavaScript moderno compatible con Node.js (versión 18+), Deno, Bun y los navegadores actuales sin necesidad de bundler adicional, aunque puedes usar uno si lo necesitas para el frontend.

Código Gleam que funciona en los dos targets

La mayor parte de la librería estándar de Gleam funciona en ambos targets. gleam/list, gleam/string, gleam/result, gleam/option... todo eso compila igual a Erlang que a JavaScript. Si tu lógica de negocio no depende de APIs específicas de plataforma, puedes reutilizarla en los dos entornos:

import gleam/list
import gleam/string

pub fn process_names(names: List(String)) -> List(String) {
  names
  |> list.filter(fn(name) { string.length(name) > 0 })
  |> list.map(fn(name) { string.uppercase(name) })
}

Este código funciona igual si lo compilas a Erlang para un servidor BEAM o a JavaScript para Node.js.

Interop con JavaScript: @external

Para llamar a funciones JavaScript desde Gleam usas la anotación @external. Le dices al compilador qué módulo JS importar y qué función usar:

// Llamar a una función de un módulo JS propio
@external(javascript, "./utils.mjs", "formatDate")
pub fn format_date(timestamp: Int) -> String

// Llamar a una API del navegador (envuelta en un módulo)
@external(javascript, "./dom.mjs", "getElementById")
pub fn get_element(id: String) -> Result(Element, Nil)

El fichero utils.mjs sería un módulo JavaScript normal:

// utils.mjs
export function formatDate(timestamp) {
  return new Date(timestamp).toLocaleDateString('es-ES');
}

De esta forma puedes usar cualquier librería JavaScript desde Gleam con tipos seguros en la interfaz. El compilador no sabe lo que hay dentro de la función JS, pero sí verifica que la llamas con los tipos correctos.

Targets específicos con @target

Algunas funciones solo tienen sentido en un target concreto. Puedes anotarlas con @target para que solo estén disponibles al compilar para ese entorno:

@target(javascript)
pub fn alert(message: String) -> Nil {
  do_alert(message)
}

@target(javascript)
@external(javascript, "./browser.mjs", "alert")
fn do_alert(message: String) -> Nil

Gleam con Node.js: un ejemplo práctico

Para hacer un script Node.js en Gleam, creas el proyecto normal y especificas el target en gleam.toml:

# gleam.toml
name = "mi_script"
version = "1.0.0"
target = "javascript"

[dependencies]
gleam_stdlib = ">= 0.34.0"
// src/mi_script.gleam
import gleam/io
import gleam/list

pub fn main() {
  let items = [1, 2, 3, 4, 5]
  let sum = list.fold(items, 0, fn(acc, n) { acc + n })
  io.println("Suma: " <> int.to_string(sum))
}
gleam run --target javascript
# Gleam compila y ejecuta con Node.js automáticamente

Lustre: UI en Gleam para el navegador

Lustre es el framework de UI más popular para Gleam cuando el target es el navegador. Sigue el modelo Elm (Model-View-Update): estado inmutable, mensajes tipados y una función de render pura. Si conoces Elm o la arquitectura de Redux, el modelo te resultará familiar pero con el sistema de tipos de Gleam:

import lustre
import lustre/element.{text}
import lustre/element/html
import lustre/event

type Model = Int

type Msg {
  Increment
  Decrement
}

fn update(model: Model, msg: Msg) -> Model {
  case msg {
    Increment -> model + 1
    Decrement -> model - 1
  }
}

fn view(model: Model) {
  html.div([], [
    html.button([event.on_click(Decrement)], [text("-")]),
    html.p([], [text(int.to_string(model))]),
    html.button([event.on_click(Increment)], [text("+")]),
  ])
}

Cuándo tiene sentido el target JavaScript

El target JS de Gleam no es la opción obvia para cualquier proyecto web. Si necesitas acceso al ecosistema npm completo o tienes un equipo que ya trabaja con TypeScript, probablemente TypeScript sea más práctico. Pero hay casos donde Gleam brilla: proyectos donde quieres compartir lógica de validación o de dominio entre el servidor (BEAM) y el cliente (navegador) sin duplicar código, o equipos que ya usan Gleam en el servidor y quieren consistencia de lenguaje en todo el stack.

La capacidad de compilar el mismo código a dos plataformas completamente distintas con el mismo sistema de tipos es algo que pocos lenguajes ofrecen. Para los detalles de cómo Gleam interactúa con librerías Erlang y Elixir en el target BEAM, el siguiente artículo de la serie cubre la interoperabilidad con el ecosistema Erlang.

Imagen: Pexels / Markus Spiske

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP