Currently viewing the AI version
Switch to human version

AI-Optimized Guide: Building Production-Ready REST APIs in Gleam

Technology Stack Overview

Core Components:

  • Gleam: Functional language running on BEAM VM (Erlang Virtual Machine)
  • Wisp: Web framework with built-in middleware system
  • Mist: HTTP server foundation
  • Pog: PostgreSQL client with connection pooling
  • PostgreSQL: Primary database (SQLite acceptable for prototypes only)

BEAM VM Advantages:

  • Handles millions of lightweight processes (not OS threads)
  • Built-in fault tolerance with "let it crash" philosophy
  • Automatic connection pooling and hot code reloading
  • Powers WhatsApp's 2 billion users with 50 engineers
  • Trade-off: Slow build times (30-60+ seconds minimum)

Installation and Setup

Environment Setup Requirements

macOS Installation:

brew install gleam

Ubuntu/Debian Installation:

sudo apt-get install erlang-nox
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/

Critical Installation Issues:

  • "command not found": PATH configuration problem
  • "ERTS not found" or "beam.smp: No such file": Need full Erlang/OTP package, not just runtime
  • WSL2 users: Windows PATH conflicts - use export PATH="/usr/local/bin:$PATH" in .bashrc

Project Initialization

gleam new todo_api
cd todo_api
gleam add mist wisp gleam_http gleam_json gleam_erlang

Package Dependencies:

  • gleam_http: HTTP types and utilities
  • mist: HTTP server implementation
  • wisp: Web framework with middleware and routing
  • gleam_json: JSON encoding/decoding
  • gleam_erlang: Access to Erlang/OTP features

Version Pinning Strategy: Pin all versions after first successful build to prevent automatic update breakage.

Configuration Management

Secret Key Management

Development (Insecure):

let secret_key_base = wisp.random_string(64)

Production (Secure):

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)
  }
}

Critical Warning: Sessions break when restarting without persistent secret key. Users get logged out unexpectedly.

Environment Configuration

.env File Requirements:

DATABASE_URL=postgresql://postgres:password@localhost:5432/todo_api
SECRET_KEY_BASE=generate_a_real_64_character_secret_here

Database Integration

PostgreSQL Setup and Connection Pooling

Docker Setup:

docker run --name todo_db \
  -e POSTGRES_PASSWORD=password \
  -e POSTGRES_DB=todo_api \
  -p 5432:5432 \
  -d postgres

Common Port Conflicts:

  • Port 5432 already in use: Kill with sudo lsof -ti:5432 | xargs kill -9
  • macOS Postgres.app commonly squats on port 5432
  • Alternative: Use different port like -p 5433:5432

Connection Pool Configuration:

pub fn create_pool(pool_size: Int) -> Result(pog.Config, DatabaseError) {
  use db_url <- result.try(get_db_config())
  
  case pog.url_config(db_url) {
    Ok(config) -> 
      Ok(config |> pog.pool_size(pool_size) |> pog.default_timeout(5000))
    Error(_) -> 
      Error(ConfigError("Invalid DATABASE_URL format"))
  }
}

Memory Usage Impact:

  • BEAM apps: 200MB base memory
  • Additional 50MB per 10 connections
  • Start with 10 connections, increase when getting "no available connections" errors

Database Schema and Operations

Schema Creation:

CREATE TABLE todos (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,
  completed BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Index Strategy: Don't add indexes initially. Wait for real data to identify slow queries.

Query Implementation:

pub fn list_todos(db: pog.Connection) -> List(Todo) {
  let sql = "SELECT id, title, completed, created_at FROM todos ORDER BY created_at DESC"
  
  case pog.query(sql) |> pog.returning(todo.todo_decoder()) |> pog.execute(db) {
    Ok(response) -> response.rows
    Error(_) -> []  // Return empty list on error
  }
}

Error Handling and Production Failures

Common Production Failures

JSON Decoding Failures:

  • Cause: Mobile clients send strings as numbers inconsistently
  • iOS: Stringifies everything
  • Android: Sometimes sends numbers, sometimes strings
  • Solution: Never use assert in production, always pattern match on Result types

Database Connection Failures:

pub fn with_db_retry(db_operation: fn(pog.Connection) -> Result(a, DatabaseError)) -> Result(a, DatabaseError) {
  case db_operation() {
    Ok(result) -> Ok(result)
    Error(DatabaseFailure(_)) as error -> {
      io.println("Database operation failed, retrying...")
      process.sleep(100)
      db_operation()
    }
    Error(other) -> Error(other)
  }
}

PostgreSQL Log Management:

  • Critical Issue: PostgreSQL logs can consume entire disk space
  • Real Incident: 20GB of query logs caused midnight API failure with "ENOSPC: no space left on device"
  • Solution: Implement log rotation immediately

Request Validation and Security

JSON Validation Pattern:

// Wrong - will crash on invalid JSON
let assert Ok(todo) = dynamic.from(json) |> create_todo_decoder()

// Right - handle parsing errors gracefully
case dynamic.from(json) |> create_todo_decoder() {
  Ok(todo) -> handle_valid_todo(todo)
  Error(decode_errors) -> {
    let error_msg = "Invalid JSON: " <> string.inspect(decode_errors)
    wisp.bad_request() |> wisp.json_body(error_response(error_msg))
  }
}

File Upload Security:

pub fn handle_file_upload(req: Request) -> Response {
  use formdata <- wisp.require_multipart_body(req)
  
  case list.key_find(formdata, "file") {
    Ok(wisp.File(filename, content)) -> {
      // Validate file size (max 10MB)
      case bit_array.byte_size(content) > 10_000_000 {
        True -> wisp.request_entity_too_large()
        False -> process_uploaded_file(filename, content)
      }
    }
    _ -> wisp.bad_request() |> wisp.json_body(error_json("No file provided"))
  }
}

Security Requirements:

  • Validate file types to prevent executable uploads
  • Store files outside web root
  • Never use wildcards (*) in CORS origins for production

API Design Patterns

Rate Limiting Implementation

In-Memory Rate Limiter:

pub fn rate_limit_middleware(
  req: Request,
  handle_request: fn(Request) -> Response,
  limit_per_minute: Int,
) -> Response {
  let client_ip = get_client_ip(req)
  let current_minute = get_current_minute()
  let key = client_ip <> ":" <> int.to_string(current_minute)
  
  case get_request_count(key) {
    count if count >= limit_per_minute -> {
      wisp.too_many_requests()
      |> wisp.set_header("retry-after", "60")
      |> wisp.json_body(error_json("Rate limit exceeded"))
    }
    count -> {
      increment_request_count(key)
      handle_request(req)
    }
  }
}

Critical Issue: In-memory rate limiting fails with multiple servers. Use Redis for distributed systems.

CORS Configuration

Development CORS:

fn add_cors_headers(response: Response) -> Response {
  response
  |> wisp.set_header("access-control-allow-origin", "*")
  |> wisp.set_header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS")
  |> wisp.set_header("access-control-allow-headers", "content-type, authorization")
  |> wisp.set_header("access-control-max-age", "86400")
}

Production Warning: Replace "*" with actual domains. Wildcards defeat CORS security purpose.

API Versioning Strategy

pub fn handle_request(req: Request) -> Response {
  case wisp.path_segments(req) {
    ["api", "v1", ..rest] -> handle_v1_routes(req, rest)
    ["api", "v2", ..rest] -> handle_v2_routes(req, rest)
    _ -> wisp.not_found()
  }
}

Deprecation Policy: Support old versions for minimum one year with documented deprecation dates.

Performance and Monitoring

Performance Characteristics

BEAM/Gleam Trade-offs:

  • Strength: Consistent latency under load, handles thousands of concurrent connections
  • Weakness: Not optimized for raw per-request throughput
  • Use Case: Better for WebSocket servers than high-throughput REST APIs

Common Bottlenecks:

  1. Database queries (use indexes and connection pooling)
  2. JSON serialization for large objects
  3. Memory allocation for temporary objects
  4. External API calls without connection pooling

Logging and Monitoring

Structured Logging:

pub fn log_api_request(
  req: Request,
  response_time_ms: Int,
  status_code: Int,
) -> Nil {
  json.object([
    #("timestamp", json.string(get_iso_timestamp())),
    #("method", json.string(http.method_to_string(req.method))),
    #("path", json.string(wisp.path(req))),
    #("status_code", json.int(status_code)),
    #("response_time_ms", json.int(response_time_ms)),
    #("user_agent", json.string(get_user_agent(req))),
  ])
  |> json.to_string
  |> io.println
}

Metrics Endpoint: Add /metrics endpoint for Prometheus or monitoring tool integration.

Framework Comparison Matrix

Feature Wisp Mist Lustre Cowboy Adapter
Primary Use REST APIs, web services Low-level HTTP handling Browser applications High-performance HTTP
Learning Difficulty Easy Medium Medium Hard (requires Erlang knowledge)
Middleware Support ✅ Built-in ❌ Manual ❌ Frontend focus ✅ Via Erlang ecosystem
JSON Handling ✅ Built-in helpers ⚠️ Manual ✅ Frontend data ⚠️ Manual
WebSocket Support ❌ Not built-in ✅ Full API ✅ Frontend WebSockets ✅ Full support
Session Management ✅ Signed cookies ❌ Manual ❌ Frontend only ⚠️ Via Erlang libraries
CORS Support ⚠️ Manual middleware ❌ Manual headers ❌ Browser handles ❌ Manual implementation
Production Maturity ✅ Ready ✅ Battle tested ⚠️ Evolving ✅ Very mature

Resource Requirements and Decision Criteria

Time Investment

  • Initial Setup: 2-4 hours for basic API
  • Database Integration: 4-8 hours including error handling
  • Production Deployment: 8-16 hours with monitoring and security
  • Learning Curve: 1-2 weeks for developers familiar with functional programming

Expertise Requirements

  • Minimum: Basic functional programming concepts
  • Recommended: Erlang/OTP understanding for production debugging
  • Critical: PostgreSQL administration for production databases

When to Choose Gleam

  • Ideal: High-concurrency applications, WebSocket servers, fault-tolerant systems
  • Avoid: High-throughput REST APIs, rapid prototyping, small teams without functional programming experience

Alternative Technologies

  • Higher Performance: Go, Rust for raw throughput
  • Faster Development: Node.js, Python Flask for rapid prototyping
  • Similar Architecture: Elixir/Phoenix for more mature ecosystem

Critical Production Warnings

  1. Never use assert in production code - will crash on invalid input
  2. Implement log rotation immediately - PostgreSQL logs will fill disk
  3. Use connection pooling from start - avoid connection exhaustion
  4. Pin dependency versions - automatic updates break builds
  5. Validate all file uploads - prevent executable file uploads
  6. Use Redis for distributed rate limiting - in-memory fails with multiple servers
  7. Set proper CORS origins in production - wildcards defeat security
  8. Monitor disk space - database logs grow rapidly
  9. Implement proper secret key management - avoid session breakage on restart
  10. Test mobile client compatibility - iOS/Android send inconsistent JSON formats

Useful Links for Further Investigation

Essential Resources for Gleam REST API Development

LinkDescription
Gleam Writing GuideHow to structure Gleam projects without making a mess. Read this first.
Wisp Web Framework DocumentationThe web framework docs. Actually has working examples, unlike most documentation.
Pog PostgreSQL Client DocumentationPostgreSQL client that doesn't suck. Connection pooling examples that actually work.
Gleam Language TourInteractive tutorial. Learn the language without installing anything.
Mist HTTP ServerHTTP server underneath Wisp. Read this if Wisp isn't flexible enough.
Gleam HTTP PackageHTTP types. Request, Response, Method enums. Basic stuff.
Gleam JSON DocumentationJSON handling that won't crash when clients send weird data. Mostly.
Gleam Dynamic DocumentationRuntime type checking for when you can't trust the input. Which is always.
Pog GitHub RepositoryReal examples of PostgreSQL usage. Docker setup that actually works.
Sqlight SQLite ClientSQLite client for when PostgreSQL is overkill. Good for prototypes.
Gleam Erlang Process DocumentationBEAM process management. Connection pooling, supervisors, the works.
Envoy Environment VariablesEnvironment variable handling. Database URLs, API keys, config stuff.
Building HTTP/JSON API in Gleam TutorialRecent tutorial that covers the basics. Project setup to deployment.
Building Your First Gleam Web AppFull-stack Gleam tutorial. API + frontend integration patterns.
Gleam Package IndexFind Gleam packages. Filter by category to find what you need.
Awesome Gleam Resource ListCommunity list of Gleam resources. Actually maintained.
BEAM Deployment PatternsBEAM deployment guide. Release management, monitoring, production stuff.
PostgreSQL Performance TuningPostgreSQL performance tuning. Query optimization, indexing, config.
Fly.io BEAM Application GuideDeploy BEAM apps on Fly.io. Environment, database, monitoring setup.
BEAM VM DocumentationBEAM VM docs. Concurrency, processes, troubleshooting when things break.
Gleeunit Testing FrameworkBuilt-in testing framework. Unit tests, integration tests, organization.
Gleam Language ServerIDE setup for VS Code, Vim, etc. Autocomplete, error checking, refactoring.
Gleam Command Line Reference - FormatCode formatter. Keep your code consistent across the team.
Gleam Discord CommunityDiscord for help with problems. Beginner-friendly community.
Gleam GitHub OrganizationOfficial GitHub repos. Compiler, stdlib, frameworks. Follow development.
Exercism Gleam TrackCoding exercises for learning Gleam. JSON, errors, validation practice.
Gleam Weekly NewsletterWeekly newsletter. New packages, tutorials, ecosystem updates.
Hex Package ManagerHex package manager. Battle-tested libraries for auth, monitoring, integrations.
Erlang Efficiency GuideBEAM performance guide. Memory management, process optimization, tuning.
OTP Design PrinciplesOTP design patterns. Supervision trees, process management, fault tolerance.

Related Tools & Recommendations

integration
Recommended

GitOps Integration Hell: Docker + Kubernetes + ArgoCD + Prometheus

How to Wire Together the Modern DevOps Stack Without Losing Your Sanity

kubernetes
/integration/docker-kubernetes-argocd-prometheus/gitops-workflow-integration
100%
integration
Recommended

Kafka + MongoDB + Kubernetes + Prometheus Integration - When Event Streams Break

When your event-driven services die and you're staring at green dashboards while everything burns, you need real observability - not the vendor promises that go

Apache Kafka
/integration/kafka-mongodb-kubernetes-prometheus-event-driven/complete-observability-architecture
81%
compare
Recommended

Python vs JavaScript vs Go vs Rust - Production Reality Check

What Actually Happens When You Ship Code With These Languages

rust
/compare/python-javascript-go-rust/production-reality-check
78%
tool
Recommended

rust-analyzer - Finally, a Rust Language Server That Doesn't Suck

After years of RLS making Rust development painful, rust-analyzer actually delivers the IDE experience Rust developers deserve.

rust-analyzer
/tool/rust-analyzer/overview
57%
news
Recommended

Google Avoids Breakup but Has to Share Its Secret Sauce

Judge forces data sharing with competitors - Google's legal team is probably having panic attacks right now - September 2, 2025

rust
/news/2025-09-02/google-antitrust-ruling
57%
tool
Recommended

Podman Desktop - Free Docker Desktop Alternative

competes with Podman Desktop

Podman Desktop
/tool/podman-desktop/overview
53%
pricing
Recommended

Should You Use TypeScript? Here's What It Actually Costs

TypeScript devs cost 30% more, builds take forever, and your junior devs will hate you for 3 months. But here's exactly when the math works in your favor.

TypeScript
/pricing/typescript-vs-javascript-development-costs/development-cost-analysis
49%
integration
Recommended

RAG on Kubernetes: Why You Probably Don't Need It (But If You Do, Here's How)

Running RAG Systems on K8s Will Make You Hate Your Life, But Sometimes You Don't Have a Choice

Vector Databases
/integration/vector-database-rag-production-deployment/kubernetes-orchestration
47%
tool
Recommended

GitHub Actions Marketplace - Where CI/CD Actually Gets Easier

integrates with GitHub Actions Marketplace

GitHub Actions Marketplace
/tool/github-actions-marketplace/overview
47%
alternatives
Recommended

GitHub Actions Alternatives That Don't Suck

integrates with GitHub Actions

GitHub Actions
/alternatives/github-actions/use-case-driven-selection
47%
integration
Recommended

GitHub Actions + Docker + ECS: Stop SSH-ing Into Servers Like It's 2015

Deploy your app without losing your mind or your weekend

GitHub Actions
/integration/github-actions-docker-aws-ecs/ci-cd-pipeline-automation
47%
tool
Recommended

Erlang/OTP - The Weird Functional Language That Handles Millions of Connections

While your Go service crashes at 10k users, Erlang is over here spawning processes cheaper than you allocate objects

Erlang/OTP
/tool/erlang-otp/overview
46%
tool
Recommended

containerd - The Container Runtime That Actually Just Works

The boring container runtime that Kubernetes uses instead of Docker (and you probably don't need to care about it)

containerd
/tool/containerd/overview
43%
alternatives
Recommended

MongoDB Alternatives: Choose the Right Database for Your Specific Use Case

Stop paying MongoDB tax. Choose a database that actually works for your use case.

MongoDB
/alternatives/mongodb/use-case-driven-alternatives
39%
tool
Recommended

Podman - The Container Tool That Doesn't Need Root

Runs containers without a daemon, perfect for security-conscious teams and CI/CD pipelines

Podman
/tool/podman/overview
30%
pricing
Recommended

Docker, Podman & Kubernetes Enterprise Pricing - What These Platforms Actually Cost (Hint: Your CFO Will Hate You)

Real costs, hidden fees, and why your CFO will hate you - Docker Business vs Red Hat Enterprise Linux vs managed Kubernetes services

Docker
/pricing/docker-podman-kubernetes-enterprise/enterprise-pricing-comparison
30%
alternatives
Recommended

Podman Desktop Alternatives That Don't Suck

Container tools that actually work (tested by someone who's debugged containers at 3am)

Podman Desktop
/alternatives/podman-desktop/comprehensive-alternatives-guide
30%
news
Recommended

VS Code 1.103 Finally Fixes the MCP Server Restart Hell

Microsoft just solved one of the most annoying problems in AI-powered development - manually restarting MCP servers every damn time

Technology News Aggregation
/news/2025-08-26/vscode-mcp-auto-start
30%
integration
Recommended

GitHub Copilot + VS Code Integration - What Actually Works

Finally, an AI coding tool that doesn't make you want to throw your laptop

GitHub Copilot
/integration/github-copilot-vscode/overview
30%
review
Recommended

Cursor AI Review: Your First AI Coding Tool? Start Here

Complete Beginner's Honest Assessment - No Technical Bullshit

Cursor
/review/cursor-vs-vscode/first-time-user-review
30%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization