Install Gleam Without Getting Fucked by Dependencies
First things first - get Gleam and Erlang installed. Gleam runs on the BEAM VM (same as WhatsApp's backend), so you need Erlang first.
macOS (the easy way):
brew install gleam
Ubuntu/Debian (the annoying way):
sudo apt-get install erlang-nox
## Check latest release first, I'm using v1.3.2 but grab whatever's newest
## Official releases: https://github.com/gleam-lang/gleam/releases
## Installation guide: https://gleam.run/getting-started/installing/
wget https://github.com/gleam-lang/gleam/releases/download/v1.3.2/gleam-v1.3.2-x86_64-unknown-linux-musl.tar.gz
tar -xzf gleam-v1.3.2-x86_64-unknown-linux-musl.tar.gz
sudo mv gleam /usr/local/bin/
Make sure it works:
gleam --version
## Should show v1.3.2 or whatever version you downloaded
If you get "command not found", check your PATH. If you get weird Erlang errors like "ERTS not found" or "beam.smp: No such file", you probably need to install the full Erlang/OTP package, not just the runtime. Ubuntu's erlang-nox
is the minimal one that actually works. WSL2 users: the Windows PATH fucks with everything - use export PATH="/usr/local/bin:$PATH"
in your .bashrc. More troubleshooting tips here.
Create a New Project That Actually Works
gleam new todo_api
cd todo_api
gleam add mist wisp gleam_http gleam_json gleam_erlang
Don't pin versions unless something breaks. Latest Gleam with these packages works fine. I once had our build break because of an automatic update to gleam_json - now I pin everything. Here's what you just installed:
- gleam_http: HTTP types so you don't have to write them
- mist: The HTTP server that doesn't suck
- wisp: Web framework with middleware and cookies
- gleam_json: JSON handling without wanting to die
- gleam_erlang: Access to Erlang/OTP goodies
Check the Gleam package index for the latest versions and Hex docs for API documentation.
Why BEAM Matters (And Why Your Node.js Backend is Slow)
Gleam runs on BEAM - the same virtual machine powering WhatsApp's 2 billion users. While you're manually managing connection pools in Express, BEAM gives you:
- Lightweight processes - millions of them, not OS threads that eat RAM (Erlang processes vs threads)
- Fault tolerance - one broken request won't kill your server (Let it crash philosophy)
- Built-in connection pooling - no more "connection limit exceeded" errors
- Hot code reloading - deploy without downtime (Code reloading docs)
The catch? Gleam builds are slow as hell. Budget 30-60 seconds minimum, sometimes way longer if the build gods hate you that day. See performance tips for optimization strategies.
Build Your First HTTP Server
Replace src/todo_api.gleam
with this:
import gleam/erlang/process
import mist
import wisp
import wisp/wisp_mist
pub fn main() {
wisp.configure_logger()
// Generate random secret - you'll need a real one in production
let secret_key_base = wisp.random_string(64)
let assert Ok(_) =
wisp_mist.handler(handle_request, secret_key_base)
|> mist.new
|> mist.port(8000)
|> mist.start_http
process.sleep_forever()
}
fn handle_request(req: wisp.Request) -> wisp.Response {
case wisp.path_segments(req) {
[] -> wisp.ok() |> wisp.html_body("<h1>Todo API is running</h1>")
["api", "v1", "todos"] -> handle_todos(req)
_ -> wisp.not_found()
}
}
fn handle_todos(req: wisp.Request) -> wisp.Response {
case req.method {
Get -> {
let json = "{\"todos\": [{\"id\": \"1\", \"title\": \"Learn Gleam\", \"completed\": false}]}"
wisp.ok()
|> wisp.set_header("content-type", "application/json")
|> wisp.html_body(json)
}
_ -> wisp.method_not_allowed([Get])
}
}
Start it up:
gleam run
Visit http://localhost:8000
in your browser to see it working. Hit /api/v1/todos
for your first JSON response. Check the Wisp documentation for more routing patterns and the Mist server guide for configuration options.
The Secret Key Gotcha That'll Ruin Your Weekend
Don't forget the secret_key_base
in production. Took me way too long to figure out that sessions break when you restart without a persistent secret key. Users kept getting logged out. Oops. Read more about session management in Wisp and security best practices.
In production, load it from environment:
let secret_key = case gleam/os.get_env("SECRET_KEY_BASE") {
Ok(key) if string.length(key) >= 64 -> key
Ok(short_key) -> {
io.println("ERROR: SECRET_KEY_BASE too short, need 64+ chars")
process.halt(1)
}
Error(_) -> {
io.println("WARNING: Using insecure random key, set SECRET_KEY_BASE")
wisp.random_string(64)
}
}
JSON Handling That Won't Make You Want to Quit
Create src/app/models/todo.gleam
:
import gleam/json
import gleam/dynamic
pub type Todo {
Todo(id: String, title: String, completed: Bool)
}
pub fn todo_to_json(todo: Todo) -> json.Json {
json.object([
#("id", json.string(todo.id)),
#("title", json.string(todo.title)),
#("completed", json.bool(todo.completed)),
])
}
pub fn create_todo_decoder() -> dynamic.Decoder(Todo) {
dynamic.decode3(
Todo,
dynamic.field("id", dynamic.string),
dynamic.field("title", dynamic.string),
dynamic.field("completed", dynamic.bool),
)
}
Testing Your API (And Why It'll Probably Break)
Test your API locally once it's running - you'll hit the usual issues like CORS errors from browsers and JSON decode failures when mobile clients send strings as numbers.
Common issues you'll hit:
- CORS errors: "Access blocked by CORS policy" - browsers hate you by default
- JSON decode failures: Mobile clients love sending strings as numbers
- Connection refused: "ECONNREFUSED 127.0.0.1:8000" - server crashed, check the logs
- 500 errors: "Internal Server Error" - something fucked up in your code
The decoder gives you garbage error messages like "decode error at $.id: expected int, found string". Mobile apps love sending {"id": "123"}
when you expect {"id": 123}
. You'll spend way too long figuring out string vs number bullshit until you add proper error handling. iOS stringifies everything, Android sometimes sends actual numbers, sometimes doesn't. It's a shitshow.
Check out the JSON decoding guide for better error handling patterns and the mobile API best practices for dealing with inconsistent client behavior.