Tech Guides
01

Quick Reference

The essential commands to create, build, run, and test Gleam projects.

Project Lifecycle

Create a Project

Scaffold a new Gleam application or library.

gleam new my_app
gleam new my_lib --template lib

Run the Project

Compile and execute the main module.

gleam run
gleam run --target javascript

Run Tests

Execute all test modules in test/.

gleam test
gleam test --target javascript

Build & Check

Compile without running, or check types only.

gleam build
gleam check

Add Dependencies

Fetch packages from Hex.pm.

gleam add gleam_json
gleam add lustre --dev

Format Code

Auto-format all .gleam files.

gleam format
gleam format --check

Project Structure

my_app/
  // gleam.toml       -- project config
  // manifest.toml    -- locked dependency versions
  src/
    my_app.gleam      // main module
    my_app/
      router.gleam    // submodule: my_app/router
  test/
    my_app_test.gleam // test module

gleam.toml Essentials

name = "my_app"
version = "1.0.0"
target = "erlang"  // or "javascript"

[dependencies]
gleam_stdlib = "~> 0.44"
gleam_json = "~> 2.0"

[dev-dependencies]
gleeunit = "~> 1.0"
02

Type System

Gleam has full type inference with no null, no exceptions, and no implicit conversions. Every value has a known type at compile time.

Primitive Types

TypeExampleNotes
Int42, 1_000_000, 0xffArbitrary precision on Erlang, 64-bit float on JS
Float3.14, 1.0e-3IEEE 754 double on both targets
String"hello"UTF-8, immutable. No interpolation syntax.
BoolTrue, FalseCustom type in the prelude, not a primitive
NilNilThe unit type, returned by side-effecting functions
List(a)[1, 2, 3]Singly-linked, immutable. All elements same type.
BitArray<<1, 2, 3>>Raw bytes, Erlang binary on BEAM

Custom Types (Algebraic Data Types)

Custom types are the backbone of Gleam data modeling. They combine enums, structs, and tagged unions into one construct.

// A simple enum
pub type Direction {
  North
  South
  East
  West
}

// A record-like type with fields
pub type User {
  User(name: String, email: String, age: Int)
}

// A tagged union (sum type)
pub type Shape {
  Circle(radius: Float)
  Rectangle(width: Float, height: Float)
  Triangle(base: Float, height: Float)
}

Generics (Type Parameters)

Gleam supports parametric polymorphism through type variables. These are lowercase names in type definitions.

// A generic wrapper
pub type Box(value) {
  Box(value: value)
}

// A generic binary tree
pub type Tree(a) {
  Leaf
  Node(value: a, left: Tree(a), right: Tree(a))
}

// Generic function
pub fn identity(x: a) -> a {
  x
}

// Multiple type parameters
pub fn pair(a: a, b: b) -> #(a, b) {
  #(a, b)
}

The Result Type

Gleam has no exceptions. The Result type is how you represent operations that can fail.

// Result is defined in the prelude as:
pub type Result(value, error) {
  Ok(value)
  Error(error)
}

// Using Result in a function
pub fn parse_int(input: String) -> Result(Int, String) {
  case int.parse(input) {
    Ok(n) -> Ok(n)
    Error(_) -> Error("Not a valid integer: " <> input)
  }
}

// Option is just Result(a, Nil)
pub type Option(a) = Result(a, Nil)

Type Aliases

// Give a name to a complex type
pub type Headers = List(#(String, String))

// Aliases are fully interchangeable with the original
pub type UserId = Int
Type aliases are transparent
A UserId alias does not create a distinct type. A plain Int is accepted wherever UserId is expected. Use a custom type wrapper for true distinction.
03

Pattern Matching & Case

Pattern matching is the primary way to branch and destructure data in Gleam. All cases must be exhaustive — the compiler ensures you handle every variant.

Case Expressions

pub fn describe(shape: Shape) -> String {
  case shape {
    Circle(r) -> "Circle with radius " <> float.to_string(r)
    Rectangle(w, h) -> "Rectangle " <> float.to_string(w)
      <> " x " <> float.to_string(h)
    Triangle(b, h) -> "Triangle with base " <> float.to_string(b)
  }
}

Destructuring Patterns

// Tuple destructuring
let #(x, y, z) = #(1, 2, 3)

// List patterns
case my_list {
  [] -> "empty"
  [only] -> "one element"
  [first, ..rest] -> "head: " <> first
}

// Named field access
let User(name: name, email: email, ..) = user

// Nested patterns
case result {
  Ok(User(name: "Lucy", ..)) -> "Found Lucy!"
  Ok(user) -> "Found " <> user.name
  Error(e) -> "Error: " <> e
}

Guards & Multiple Patterns

// Guards with `if`
pub fn classify_age(age: Int) -> String {
  case age {
    a if a < 0 -> "invalid"
    a if a < 13 -> "child"
    a if a < 18 -> "teenager"
    _ -> "adult"
  }
}

// Multiple subjects
case x, y {
  0, 0 -> "origin"
  0, _ -> "y-axis"
  _, 0 -> "x-axis"
  _, _ -> "somewhere"
}

// Alternative patterns with `|`
case direction {
  North | South -> "vertical"
  East | West -> "horizontal"
}

Let & Assert Patterns

// let-assert panics if the pattern does not match
// Useful in tests or when failure is a bug
let assert Ok(value) = parse_config()

// Regular let requires an irrefutable pattern
let User(name:, ..) = get_user()  // Only works if one variant
Avoid let assert in library code
let assert causes a runtime panic on mismatch. Reserve it for tests and situations where a failed match is truly unrecoverable.
04

Modules & Imports

Every .gleam file is a module. Module paths follow the file system. Access control is simple: pub means public, no keyword means private.

Import Syntax

// Import the module, call functions as module.function
import gleam/list
import gleam/string

pub fn main() {
  list.map([1, 2, 3], fn(x) { x * 2 })
  string.uppercase("hello")
}

// Alias a module
import gleam/string as str

// Unqualified imports (bring names into scope)
import gleam/list.{map, filter, fold}
import gleam/option.{type Option, Some, None}

Access Control

KeywordApplies ToVisibility
pub fnFunctionsCallable from other modules
fnFunctionsPrivate to this module
pub typeTypesType and constructors are public
pub opaque typeTypesType is public, constructors are private
pub constConstantsVisible from other modules

Opaque Types

Opaque types expose the type name but hide the constructors, enforcing construction through validated functions.

// In email.gleam
pub opaque type Email {
  Email(String)
}

pub fn parse(raw: String) -> Result(Email, String) {
  case string.contains(raw, "@") {
    True -> Ok(Email(raw))
    False -> Error("Invalid email")
  }
}

pub fn to_string(email: Email) -> String {
  let Email(raw) = email
  raw
}

Module Paths

// File: src/my_app/web/router.gleam
// Module: my_app/web/router

import my_app/web/router
import my_app/db/user as user_db
Naming convention
Module names use snake_case. Types use PascalCase. Functions and variables use snake_case. Constants use snake_case.
05

The Pipe Operator

The |> operator passes the result of the left side as the first argument to the function on the right. It transforms deeply nested calls into readable pipelines.

Basic Piping

// Without pipes (nested, hard to read)
string.reverse(string.uppercase(string.trim("  hello  ")))

// With pipes (left to right, clear flow)
"  hello  "
|> string.trim
|> string.uppercase
|> string.reverse
// => "OLLEH"

Piping with Additional Arguments

// The pipe fills the FIRST argument
[1, 2, 3, 4, 5]
|> list.filter(fn(x) { x > 2 })
|> list.map(fn(x) { x * 10 })
|> list.fold(0, int.add)
// => 120

// Equivalent to:
list.fold(list.map(list.filter([1,2,3,4,5], fn(x) { x > 2 }), fn(x) { x * 10 }), 0, int.add)

Piping into Anonymous Functions

42
|> int.to_string
|> fn(s) { "The answer is: " <> s }
// Note: this calls the anonymous function with "42"

Real-World Pipeline

pub fn handle_request(req: Request) -> Response {
  req
  |> authenticate
  |> result.then(authorize)
  |> result.then(validate_body)
  |> result.map(process_data)
  |> to_response
}
Design for pipes
Gleam's standard library puts the "subject" as the first parameter of every function. When writing your own functions, follow this convention so they compose naturally with |>.
06

Error Handling

Gleam uses Result for all fallible operations. No exceptions, no try/catch. The use expression eliminates callback nesting when chaining Results.

Result Chaining with result.then

import gleam/result

pub fn get_user_email(id: Int) -> Result(String, String) {
  id
  |> find_user
  |> result.then(validate_active)
  |> result.map(fn(user) { user.email })
}

// result.then: (Result(a, e), fn(a) -> Result(b, e)) -> Result(b, e)
// result.map:  (Result(a, e), fn(a) -> b) -> Result(b, e)

The use Expression

use is syntactic sugar that flattens callback chains. It rewrites the rest of the block into a callback passed to the right-hand function.

// Without `use` — deeply nested callbacks
pub fn register(data: FormData) -> Result(User, String) {
  result.then(validate_name(data.name), fn(name) {
    result.then(validate_email(data.email), fn(email) {
      result.then(validate_age(data.age), fn(age) {
        Ok(User(name:, email:, age:))
      })
    })
  })
}

// With `use` — flat and readable
pub fn register(data: FormData) -> Result(User, String) {
  use name <- result.then(validate_name(data.name))
  use email <- result.then(validate_email(data.email))
  use age <- result.then(validate_age(data.age))
  Ok(User(name:, email:, age:))
}

How use Works

The compiler transforms use x <- f(a) so that everything below it becomes a callback function passed as the last argument to f.

// This:
use x <- result.then(some_result())
do_something(x)

// Becomes:
result.then(some_result(), fn(x) {
  do_something(x)
})
use is not just for Results
use works with any function that takes a callback as its last argument: result.then, list.each, bool.guard, or your own functions.

Common Result Utilities

FunctionSignaturePurpose
result.map(Result(a, e), fn(a) -> b) -> Result(b, e)Transform the Ok value
result.then(Result(a, e), fn(a) -> Result(b, e)) -> Result(b, e)Chain fallible operations
result.map_error(Result(a, e), fn(e) -> f) -> Result(a, f)Transform the Error value
result.unwrap(Result(a, e), a) -> aExtract with a default
result.try_recover(Result(a, e), fn(e) -> Result(a, f)) -> Result(a, f)Attempt recovery from error
result.all(List(Result(a, e))) -> Result(List(a), e)Collect all Oks or first Error

bool.guard for Early Returns

import gleam/bool

pub fn divide(a: Float, b: Float) -> Result(Float, String) {
  use <- bool.guard(b == 0.0, Error("Division by zero"))
  Ok(a /. b)
}
07

FFI with Erlang & Elixir

Gleam compiles to Erlang and runs on the BEAM. You can call any Erlang or Elixir function using the Foreign Function Interface.

Calling Erlang Functions

Use @external attributes to declare functions implemented in Erlang.

// In your .gleam file, declare the external function
@external(erlang, "erlang", "system_time")
pub fn system_time() -> Int

// Call an Erlang module function
@external(erlang, "crypto", "strong_rand_bytes")
pub fn random_bytes(count: Int) -> BitArray

// Call an Erlang function from a custom module
@external(erlang, "my_app_native", "hash_password")
pub fn hash_password(password: String) -> String

Erlang FFI Files

Place Erlang source files alongside your Gleam code. They must share the module name.

%% File: src/my_app/native_ffi.erl
-module(my_app@native_ffi).
-export([hash_password/1]).

hash_password(Password) ->
    crypto:hash(sha256, Password).
// File: src/my_app/native_ffi.gleam
@external(erlang, "my_app@native_ffi", "hash_password")
pub fn hash_password(password: String) -> BitArray
Type safety at the boundary
The Gleam compiler trusts your @external type signatures. An incorrect signature will compile but crash at runtime. Test FFI boundaries thoroughly.

Calling Elixir Functions

// Elixir modules are atoms prefixed with "Elixir."
@external(erlang, "Elixir.Jason", "encode!")
pub fn json_encode(data: Dynamic) -> String

@external(erlang, "Elixir.Enum", "sort")
pub fn sort_list(list: List(a)) -> List(a)

Dynamic Type for FFI

When receiving untyped data from Erlang/Elixir, use Dynamic and decoders.

import gleam/dynamic/decode

@external(erlang, "os", "getenv")
fn getenv_ffi(name: String) -> Dynamic

pub fn get_env(name: String) -> Result(String, Nil) {
  name
  |> getenv_ffi
  |> decode.run(decode.string)
  |> result.nil_error
}
08

Gleam on JavaScript

Gleam can compile to JavaScript, running in Node.js, Deno, Bun, or the browser. The same code can target both Erlang and JavaScript.

Targeting JavaScript

// In gleam.toml
target = "javascript"

// Or via CLI flags
gleam run --target javascript
gleam build --target javascript
gleam test --target javascript

JavaScript FFI

Create .mjs files alongside your Gleam modules for JS-specific implementations.

// File: src/my_app/dom_ffi.mjs
export function getElementById(id) {
  const el = document.getElementById(id);
  if (el === null) return new Error(undefined);
  return new Ok(el);
}

export function alert(message) {
  window.alert(message);
}
// File: src/my_app/dom_ffi.gleam
@external(javascript, "./dom_ffi.mjs", "getElementById")
pub fn get_element_by_id(id: String) -> Result(Element, Nil)

@external(javascript, "./dom_ffi.mjs", "alert")
pub fn alert(message: String) -> Nil

Conditional Compilation

Provide different implementations for each target using dual @external attributes.

// Erlang implementation
@external(erlang, "erlang", "system_time")
// JavaScript implementation
@external(javascript, "./time_ffi.mjs", "now")
pub fn now() -> Int
// time_ffi.mjs
export function now() {
  return Date.now();
}

Lustre: Gleam for the Browser

Lustre is the leading Gleam framework for building web UIs, inspired by Elm.

import lustre
import lustre/element/html
import lustre/event

pub type Msg {
  Increment
  Decrement
}

fn init(_) -> Int { 0 }

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

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

pub fn main() {
  let app = lustre.simple(init, update, view)
  let assert Ok(_) = lustre.start(app, "#app", Nil)
}
09

OTP Interop

On the Erlang target, Gleam has full access to OTP — processes, supervisors, and message passing. The gleam_otp and gleam_erlang packages provide typed APIs.

Processes & Subjects

Gleam wraps Erlang processes with typed Subject values for safe message passing.

import gleam/erlang/process.{type Subject}

// A Subject(msg) is a typed handle to send messages to a process

// Start a process that receives String messages
pub fn start_logger() -> Subject(String) {
  process.new_subject()
}

// Send a typed message
process.send(logger, "Hello from Gleam!")

// Receive with a timeout
let assert Ok(msg) = process.receive(subject, 5000)

Actors (gleam_otp)

The actor module provides a higher-level abstraction over OTP gen_server, with typed state and messages.

import gleam/otp/actor

pub type Msg {
  Increment
  GetCount(reply_to: Subject(Int))
  Shutdown
}

fn handle_message(msg: Msg, count: Int) -> actor.Next(Msg, Int) {
  case msg {
    Increment -> actor.continue(count + 1)
    GetCount(client) -> {
      process.send(client, count)
      actor.continue(count)
    }
    Shutdown -> actor.Stop(process.Normal)
  }
}

pub fn start_counter() {
  actor.start(0, handle_message)
}

Supervisors

import gleam/otp/supervisor

pub fn start() {
  supervisor.start(fn(children) {
    children
    |> supervisor.add(supervisor.worker(fn(_) {
      start_counter()
    }))
    |> supervisor.add(supervisor.worker(fn(_) {
      start_logger()
    }))
  })
}

Selectors & Multiple Message Sources

import gleam/erlang/process.{type Selector}

// A Selector lets a process wait on messages from multiple subjects
let selector =
  process.new_selector()
  |> process.selecting(subject_a, fn(msg) { FromA(msg) })
  |> process.selecting(subject_b, fn(msg) { FromB(msg) })

// Wait for the next message from any source
let assert Ok(msg) = process.select(selector, 5000)
Erlang target only
OTP features (gleam_otp, gleam_erlang) only work on the Erlang target. The JavaScript target has no process model.
10

Package Ecosystem

Gleam packages are published to Hex.pm and documented on HexDocs. The Gleam compiler handles dependency resolution directly.

Essential Packages

PackagePurposeTarget
gleam_stdlibCore types, list, string, int, float, result, option, dictBoth
gleam_jsonJSON encoding and decodingBoth
gleam_httpHTTP types (Request, Response, Method, Header)Both
gleam_otpActors, supervisors, tasksErlang
gleam_erlangBEAM process primitives, atoms, ETSErlang
lustreElm-style web framework (browser + server components)Both
wispWeb framework for building HTTP serversErlang
mistHTTP server built on OTPErlang
gleeunitSimple test runnerBoth
birdieSnapshot testingBoth
argvCommand-line argument parsingBoth
envoyEnvironment variable accessBoth
simplifileFile system operationsBoth

Managing Dependencies

// Add a package
gleam add wisp
gleam add gleeunit --dev

// Remove a package
gleam remove wisp

// Update all dependencies to latest compatible versions
gleam update

// Update a specific package
gleam update gleam_json

// View dependency tree
gleam deps list

Publishing a Package

// Ensure gleam.toml has required fields
name = "my_package"
version = "1.0.0"
description = "A useful Gleam library"
licences = ["Apache-2.0"]

[repository]
type = "github"
user = "your-name"
repo = "my_package"

// Publish to hex.pm
gleam publish

// Generate documentation
gleam docs build
gleam docs publish

A Complete Web Server

Here is a minimal Gleam web application using Wisp and Mist, showing how the ecosystem fits together.

import gleam/bytes_tree
import gleam/http/response
import mist
import wisp
import wisp/wisp_mist

pub fn main() {
  let secret_key = wisp.random_string(64)

  let handler = fn(req) {
    use req <- wisp_mist.handler(req, secret_key)
    handle_request(req)
  }

  let assert Ok(_) =
    handler
    |> 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.string_body("Hello from Gleam!")
    ["api", "health"] -> wisp.ok()
      |> wisp.json_body("{\"status\": \"ok\"}")
    _ -> wisp.not_found()
  }
}

Testing with Gleeunit

// test/my_app_test.gleam
import gleeunit
import gleeunit/should
import my_app

pub fn main() {
  gleeunit.main()
}

pub fn add_test() {
  my_app.add(2, 3)
  |> should.equal(5)
}

pub fn parse_email_test() {
  my_app.parse_email("lucy@example.com")
  |> should.be_ok

  my_app.parse_email("not-an-email")
  |> should.be_error
}
Discover packages
Browse all Gleam packages at packages.gleam.run or search Hex.pm directly. The Gleam package index shows download counts, documentation links, and target compatibility.