Dream Logo

Clean, composable web development for Gleam. No magic.

Dream is a web toolkit that gets out of your way. Everything is explicit. Your main() function shows exactly what’s happening—no framework deciding things for you behind the scenes.

Latest Release

Why Dream?

Because you’ve debugged enough “helpful” frameworks at 2am.

Here’s a complete web application:

import dream/servers/mist/server.{bind, listen, router, services} as dream
import router.{create_router}
import services.{initialize_services}

pub fn main() {
  dream.new()
  |> services(initialize_services())
  |> router(create_router())
  |> bind("localhost")
  |> listen(3000)
}

That’s it. No hidden configuration. No magic middleware appearing from nowhere. Everything you need to understand your app is right there in main().

Why Not a Framework?

How many times have you learned how to do authentication? Routing? Database queries? Validation?

Not the concepts—the framework’s way of doing them. The Rails Way. The Django Way. The Spring Way. Knowledge that evaporates the moment you switch languages or projects.

Frameworks teach you framework-specific solutions:

Dream provides patterns without enforcement:

Controllers are functions. Models are functions. Views are functions. You already know how functions work.

The knowledge you build using Dream—explicit dependencies, pure functions, separation of concerns—transfers to any language, any project, any team. Because it’s fundamental software engineering, not framework minutiae.

No framework lock-in. No knowledge that expires when you switch stacks.

How It Flows

When a request arrives:

1. Request arrives
2. Context created (AppContext with unique request_id)
3. Router matches path/method → finds controller
4. Middleware runs (if any) → can enrich context
5. Controller executes with (Request, Context, Services)
6. Returns Response

Example with auth and logging (your middleware, not Dream’s):

sequenceDiagram
    autonumber
    participant Client
    participant Router
    participant Logging as Logging Middleware
    participant Auth as Auth Middleware
    participant Controller as Controller Action
    
    Client->>Router: Request
    Router->>Router: Create Context with request_id
    Router->>Logging: (Request, Context, Services)
    Logging->>Logging: Log incoming request
    Logging->>Auth: (Request, Context, Services)
    Auth->>Auth: Check token, add user to context
    Auth->>Controller: (Request, Context with user, Services)
    Controller->>Controller: Do work
    Controller-->>Auth: Response
    Auth-->>Logging: Response
    Logging->>Logging: Log outgoing response
    Logging-->>Router: Response
    Router-->>Client: Response

Simple pipeline. No magic.

The Pieces

At its core, Dream is a router. Around it are helpers for common needs.


Router (the core)

Pattern matches requests to controllers:

Router(routes: List(Route(context, services)))

Route(
  method: Method,           // Get, Post, etc.
  path: String,            // "/users/:id"
  controller: fn(Request, context, services) -> Response,
  middleware: List(Middleware),
)

Supports path parameters (:id), wildcards (*, **), and middleware chains.


Controllers

In Dream, controllers are nothing more than simple functions:

fn(Request, context, services) -> Response

No base class. No decorators. Extract params, do work, return response.


Middleware

Middleware are functions you can chain to run before and after your controller:

fn(Request, context, services, next: fn(...) -> Response) -> Response

Example: route with middleware: [auth, logging] flows as:

auth (in) → logging (in) → controller → logging (out) → auth (out)

Modify request/context going in, response coming out.


Context & Services

Context is per-request data—anything that changes per-request:

Services are shared dependencies—same for all requests:

Dream starts with AppContext (just a request_id field) and EmptyServices (no dependencies). Define your own types when you need them.


HTTP Client

Gleam doesn’t have a streaming HTTPS client. That’s a problem when you need to stream AI inference responses or download large files.

We could’ve built one from scratch. Bad idea—HTTPS clients are security-critical and complex. Instead, we wrapped Erlang’s battle-hardened httpc with a clean Gleam interface:

client.new
  |> client.method(http.Get)
  |> client.scheme(http.Https)
  |> client.host("api.example.com")
  |> client.path("/stream")

Streaming and non-streaming modes. Builder pattern. Runs on the BEAM. Leverages 20+ years of Erlang HTTP client hardening.


Models & Views (optional patterns)

Models and views are just ways to organize your functions. Not required—Dream won’t stop you from doing things differently.

But here’s the thing: figuring out where code goes, how to separate concerns, and whether your pattern will scale is exhausting. Especially in a new language. These patterns work. We use them in production. They scale.

Controller calls model and view:

import dream/http.{require_int}
import dream/http/error
import utilities/response_helpers

pub fn show(request: Request, _context: Context, services: Services) -> Response {
  let result = {
    use id <- result.try(require_int(request, "id"))
    let db = services.database.connection
    use product <- result.try(product_model.get(db, id))
    Ok(product)
  }
  
  case result {
    Ok(product) -> product_view.respond(product, request)
    Error(err) -> response_helpers.handle_error(err)
  }
}

This pattern uses require_int to safely extract and validate the path parameter. If the parameter is missing or invalid, it returns a BadRequest error. The use syntax keeps the code flat and readable, avoiding nested case statements. Errors are handled uniformly through response_helpers.handle_error, which maps dream.Error types to appropriate HTTP responses.

Model handles data:

// product_model.gleam
pub fn get(db, id: Int) -> Result(Product, Error)
pub fn list(db) -> Result(List(Product), Error)
pub fn decoder() -> Decoder(ProductData)

View handles presentation:

// product_view.gleam
pub fn respond(product: Product, param: PathParam) -> Response {
  case param.format {
    Some("json") -> json_response(status.ok, to_json(product))
    Some("csv") -> text_response(status.ok, to_csv(product))
    _ -> html_response(status.ok, to_html(product))
  }
}

pub fn to_json(product: Product) -> String
pub fn to_html(product: Product) -> String
pub fn to_csv(product: Product) -> String

Controller orchestrates. Model fetches data. View formats it. All just functions. No classes. No inheritance. No framework enforcement.


Use what you need. Skip what you don’t.

Core Patterns

Beyond the basics, Dream provides patterns for scaling your application logic.

Operations (Business Logic)

Controllers should be thin. When logic gets complex (validation, multiple database calls, external services), use an Operation.

// operations/create_user.gleam
pub fn execute(services: Services, params: Params) -> Result(User, Error) {
  use _ <- result.try(validate(params))
  use user <- result.try(user_model.create(services.db, params))
  let _ = mailer.send_welcome(services.mailer, user)
  Ok(user)
}

Why? Operations are testable without HTTP. They can be reused by API endpoints, background jobs, and CLI tasks.

Multi-Format Responses (JSON + HTML)

Serve API clients and browsers from the same controller.

pub fn show(request: Request, context: Context, services: Services) -> Response {
  // ... fetch data ...
  case format {
    "json" -> json_response(ok, view.to_json(data))
    "html" -> html_response(ok, view.to_html(data))
    "htmx" -> html_response(ok, view.card(data)) // Partial for dynamic updates
  }
}

Why? Don’t build two separate backends. One app, multiple views.

What a Real App Looks Like

Here’s a complete Dream app with models, views, and controllers:

your_app/
├── src/
│   ├── main.gleam
│   ├── router.gleam
│   ├── services.gleam
│   ├── context.gleam
│   ├── config.gleam
│   │
│   ├── controllers/
│   │   ├── users_controller.gleam
│   │   └── tasks_controller.gleam
│   │
│   ├── middleware/
│   │   └── logging_middleware.gleam
│   │
│   ├── models/
│   │   ├── user/
│   │   │   ├── user_model.gleam
│   │   │   └── sql.gleam
│   │   └── task/
│   │       ├── task_model.gleam
│   │       └── sql.gleam
│   │
│   ├── views/
│   │   ├── user_view.gleam
│   │   └── task_view.gleam
│   │
│   ├── operations/
│   │   └── reorder_tasks.gleam
│   │
│   ├── templates/
│   │   ├── components/
│   │   ├── elements/
│   │   ├── layouts/
│   │   └── pages/
│   │
│   ├── services/
│   │   └── database.gleam
│   │
│   └── types/
│       └── user.gleam
│
└── gleam.toml

Everything has its place. Nothing is hidden. Scale from “Hello World” to production without restructuring.

Template Composition for Server-Side Rendering

Many developers choose front-end frameworks like React or Vue, which is perfectly fine. Dream lets you choose whatever you want for a front-end.

For those who wish to have server-side rendering with full type safety through Gleam, we have a suggested layered approach that keeps markup consistent, DRY, and reusable as applications grow.

The Problem We Solved:

In real-world applications, we’ve experienced pain points from:

The Solution:

A layered approach with four levels:

  1. Elements (templates/elements/*.matcha): Low-level HTML components

    • Reusable semantic HTML templates (buttons, inputs, cards, badges)
    • Compiled from Matcha templates
    • Classless, styled via CSS frameworks like Pico CSS
  2. Components (templates/components/*.gleam): Compose elements into reusable pieces

    • Gleam functions that combine multiple elements
    • Examples: task_card(), task_form(), project_list()
    • Handle business logic for presentation (formatting, conditional rendering)
  3. Pages (templates/pages/*.matcha or .gleam): Compose components into full pages

    • Page-level templates that combine multiple components
    • Examples: index_page(), show_page()
  4. Layouts (templates/layouts/*.gleam): Page structure (nav, footer, main wrapper)

    • Wraps pages with consistent structure
    • Handles navigation, footer, scripts
    • Example: build_page(title, content) wraps any page content

Example Flow:

// View layer (views/task_view.gleam)
pub fn index_page(tasks: List(Task), tags_by_task: List(#(Int, List(Tag)))) -> String {
  // Components compose elements
  let list = task_components.task_list(tasks, tags_by_task)
  
  // Pages compose components
  let content = index.render(task_form: "", task_list: list)
  
  // Layouts wrap pages
  layout_components.build_page("Tasks", content)
}

This pattern is by no means the only way to do server-side rendering, but we have found that it serves our needs well as our applications grow. It provides full type safety through Gleam, eliminates markup duplication, and keeps styling consistent.

See examples/tasks for a complete working example of this pattern.

Yeah, But Gleam? Really?

Fair question. Gleam is young. The ecosystem is small. Why choose it for web development?

Because Gleam runs on the BEAM. And the BEAM was literally built to solve web app problems.

What The Heck is a BEAM?

BEAM (Bogdan/Björn’s Erlang Abstract Machine) is the virtual machine that runs Erlang, Elixir, and Gleam code. Like the JVM for Java or V8 for JavaScript, but designed for completely different goals.

The JVM was built for portability. V8 was built for speed.

The BEAM was built for reliability and concurrency.

Ericsson created Erlang and the BEAM in the 1980s for telecom switches. The requirements:

Turns out, those are the exact problems web applications have.

What You Get on the BEAM

Concurrency - Millions of lightweight processes per server. Each HTTP request gets its own isolated process. No thread pools, no async/await gymnastics, no event loop bottlenecks. Just spawn a process. The BEAM’s preemptive scheduler handles the rest.

Fault tolerance - Processes crash in isolation. One request throws an error, the rest keep running. No cascading failures. No servers going down because one database query hung.

Scalability - Start with one server. Add more when traffic grows. The BEAM distributes processes across machines transparently. No architectural rewrite from monolith to microservices required to scale.

Real-world results:

And Gleam?

Gleam gives you the BEAM’s superpowers with modern developer experience:

The BEAM handles concurrency, fault tolerance, and scaling. Gleam gives you type safety and readability. You write functions.

It’s a pretty good division of labor.

“But You Can’t Hire for Gleam”

True. The Gleam job market is tiny. So is Elixir’s compared to Python or JavaScript.

But here’s what you can hire for:

And here’s the thing about good engineers: They don’t want to work on boring problems with mundane tools. They definitely don’t want to work with something shoehorned into being a solution it’s not great at (looking at you, Node.js for CPU-intensive work).

Good engineers want to learn new things. They want to work on interesting tech. They just don’t want to spend a year learning the borrow checker before they can be productive (sorry, Rust).

Gleam hits the sweet spot:

Discord runs 20+ Elixir services with 5 engineers serving 12M+ concurrent users 2. Remote reached unicorn status in ~2 years with Elixir 6. Strand ships production Gleam with zero Gleam-related crashes and a small team 7.

Your choice: hire 20 engineers to manage microservices hell in the “safe” language, or hire 5 who actually want to use the BEAM and solve problems instead of fighting infrastructure.

Quick Start

Ready to build your first Dream app? Check out the 5-Minute Quickstart.

Want to see what Dream adds over raw Mist? See the Dream vs Mist comparison.

Want to dive into working examples? See examples/ for complete applications you can run.

Documentation

Learning Path:

Guides:

Reference:

Examples:

Philosophy

Dream is explicitly not a framework. We provide:

✅ Clean interfaces and types
✅ Common patterns (controllers, models, middleware)
✅ Useful utilities (validation, response builders)
✅ Working examples

You provide:

🎯 Your application structure
🎯 Your router configuration
🎯 Your controllers and models
🎯 Your business logic

No magic. No hidden behavior. Everything explicit.

Because finding where that database connection came from shouldn’t require a treasure map.

Contributing

See Contributing Guide for guidelines.

License

MIT License


References


Built with Gleam. Runs on the BEAM. Works like you’d expect.

Search Document