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 utilitiesmist
: HTTP server implementationwisp
: Web framework with middleware and routinggleam_json
: JSON encoding/decodinggleam_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:
- Database queries (use indexes and connection pooling)
- JSON serialization for large objects
- Memory allocation for temporary objects
- 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
- Never use
assert
in production code - will crash on invalid input - Implement log rotation immediately - PostgreSQL logs will fill disk
- Use connection pooling from start - avoid connection exhaustion
- Pin dependency versions - automatic updates break builds
- Validate all file uploads - prevent executable file uploads
- Use Redis for distributed rate limiting - in-memory fails with multiple servers
- Set proper CORS origins in production - wildcards defeat security
- Monitor disk space - database logs grow rapidly
- Implement proper secret key management - avoid session breakage on restart
- Test mobile client compatibility - iOS/Android send inconsistent JSON formats
Useful Links for Further Investigation
Essential Resources for Gleam REST API Development
Link | Description |
---|---|
Gleam Writing Guide | How to structure Gleam projects without making a mess. Read this first. |
Wisp Web Framework Documentation | The web framework docs. Actually has working examples, unlike most documentation. |
Pog PostgreSQL Client Documentation | PostgreSQL client that doesn't suck. Connection pooling examples that actually work. |
Gleam Language Tour | Interactive tutorial. Learn the language without installing anything. |
Mist HTTP Server | HTTP server underneath Wisp. Read this if Wisp isn't flexible enough. |
Gleam HTTP Package | HTTP types. Request, Response, Method enums. Basic stuff. |
Gleam JSON Documentation | JSON handling that won't crash when clients send weird data. Mostly. |
Gleam Dynamic Documentation | Runtime type checking for when you can't trust the input. Which is always. |
Pog GitHub Repository | Real examples of PostgreSQL usage. Docker setup that actually works. |
Sqlight SQLite Client | SQLite client for when PostgreSQL is overkill. Good for prototypes. |
Gleam Erlang Process Documentation | BEAM process management. Connection pooling, supervisors, the works. |
Envoy Environment Variables | Environment variable handling. Database URLs, API keys, config stuff. |
Building HTTP/JSON API in Gleam Tutorial | Recent tutorial that covers the basics. Project setup to deployment. |
Building Your First Gleam Web App | Full-stack Gleam tutorial. API + frontend integration patterns. |
Gleam Package Index | Find Gleam packages. Filter by category to find what you need. |
Awesome Gleam Resource List | Community list of Gleam resources. Actually maintained. |
BEAM Deployment Patterns | BEAM deployment guide. Release management, monitoring, production stuff. |
PostgreSQL Performance Tuning | PostgreSQL performance tuning. Query optimization, indexing, config. |
Fly.io BEAM Application Guide | Deploy BEAM apps on Fly.io. Environment, database, monitoring setup. |
BEAM VM Documentation | BEAM VM docs. Concurrency, processes, troubleshooting when things break. |
Gleeunit Testing Framework | Built-in testing framework. Unit tests, integration tests, organization. |
Gleam Language Server | IDE setup for VS Code, Vim, etc. Autocomplete, error checking, refactoring. |
Gleam Command Line Reference - Format | Code formatter. Keep your code consistent across the team. |
Gleam Discord Community | Discord for help with problems. Beginner-friendly community. |
Gleam GitHub Organization | Official GitHub repos. Compiler, stdlib, frameworks. Follow development. |
Exercism Gleam Track | Coding exercises for learning Gleam. JSON, errors, validation practice. |
Gleam Weekly Newsletter | Weekly newsletter. New packages, tutorials, ecosystem updates. |
Hex Package Manager | Hex package manager. Battle-tested libraries for auth, monitoring, integrations. |
Erlang Efficiency Guide | BEAM performance guide. Memory management, process optimization, tuning. |
OTP Design Principles | OTP design patterns. Supervision trees, process management, fault tolerance. |
Related Tools & Recommendations
GitOps Integration Hell: Docker + Kubernetes + ArgoCD + Prometheus
How to Wire Together the Modern DevOps Stack Without Losing Your Sanity
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
Python vs JavaScript vs Go vs Rust - Production Reality Check
What Actually Happens When You Ship Code With These Languages
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.
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
Podman Desktop - Free Docker Desktop Alternative
competes with Podman Desktop
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.
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
GitHub Actions Marketplace - Where CI/CD Actually Gets Easier
integrates with GitHub Actions Marketplace
GitHub Actions Alternatives That Don't Suck
integrates with GitHub Actions
GitHub Actions + Docker + ECS: Stop SSH-ing Into Servers Like It's 2015
Deploy your app without losing your mind or your weekend
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
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)
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.
Podman - The Container Tool That Doesn't Need Root
Runs containers without a daemon, perfect for security-conscious teams and CI/CD pipelines
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
Podman Desktop Alternatives That Don't Suck
Container tools that actually work (tested by someone who's debugged containers at 3am)
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
GitHub Copilot + VS Code Integration - What Actually Works
Finally, an AI coding tool that doesn't make you want to throw your laptop
Cursor AI Review: Your First AI Coding Tool? Start Here
Complete Beginner's Honest Assessment - No Technical Bullshit
Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization