Gleam's type safety and functional core, plus Glimr's opinionated design mean fewer bugs, confident refactoring, and AI that actually writes correct code.
Clean, expressive, and intentional by default. Glimr helps you write code you're happy to come back to whether you're a team of 1 or 100.
/// @get "/users"
pub fn index(ctx: Context(App)) -> Response {
let users = user.list_or_fail(ctx.app.db)
response.html(user_index.render(users: users), 200)
}
/// @get "/users/:user_id"
pub fn show(ctx: Context(App), user_id: Int) -> Response {
let user = user.find_or_fail(ctx.app.db, user_id)
response.html(user_show.render(user: user), 200)
}
import app/app.{type App}
import glimr/http/context.{type Context}
import glimr/http/http.{type Response}
import glimr/http/kernel.{type Next}
pub fn run(ctx: Context(App), next: Next(App)) -> Response {
io.println("Request: " <> ctx.req.path)
// Pass context to next middleware/handler
next(ctx)
}
import app/http/middleware/logger
import glimr/http/middleware
/// @get "/users"
pub fn index(ctx: Context(App)) -> Response {
use ctx <- middleware.apply([logger.run], ctx)
let users = user.list_or_fail(ctx.app.db)
response.html(user_index.render(users: users), 200)
}
---
props(count: Int)
---
<div class="text-center">
<p class="text-2xl mb-4">Count: {{ count }}</p>
<button l-on:click="count = count - 1">-</button>
<button l-on:click="count = count + 1">+</button>
</div>
<!-- Two-way binding -->
<input l-model="name" />
<p>Hello, {{ name }}!</p>
import compiled/loom/counter
/// @get "/counter"
pub fn show(ctx: Context(App)) -> Response {
response.html(counter.render(count: 0), 200)
}
/// Define the shape of the data returned after validation
pub type Data {
Data(
name: String,
email: String,
)
}
/// Define your form's validation rules
fn rules(_ctx: Context(App)) -> List(Rule) {
[
v.for("name", [v.Required, v.MinLength(2)]),
v.for("email", [v.Required, v.Email, v.MaxLength(255)]),
]
}
/// @post "/users"
pub fn store(ctx: Context(App)) -> Response {
use data <- user_store.validate(ctx)
let user = user.create(
pool: ctx.app.db,
name: data.name,
email: data.email
)
redirect.to("/users/" <> user.id)
}
pub const table_name = "users"
pub fn definition() {
schema.table(table_name, [
schema.id(),
schema.string("email"),
schema.string("name") |> schema.nullable(),
schema.boolean("is_admin") |> schema.default_bool(False),
schema.unix_timestamps(),
])
|> schema.indexes([
schema.unique(["email"]),
])
}
-- Auto-generated by ./glimr db_gen
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
name TEXT,
is_admin INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX idx_users_email
ON users (email);
-- queries/find.sql
SELECT *
FROM users
WHERE id = $1;
-- queries/list_active.sql
SELECT *
FROM users
WHERE is_active = true
ORDER BY created_at DESC;
/// @get "/users/:user_id"
pub fn show(ctx: Context(App), user_id: Int) -> Response {
let user = user.find_or_fail(ctx.app.db, user_id)
response.html(user_show.render(user: user), 200)
}
/// @get "/users/active"
pub fn active(ctx: Context(App)) -> Response {
let users = user.list_active_or_fail(ctx.app.db)
response.html(user_index.render(users: users), 200)
}
/// @get "/dashboard"
pub fn show(ctx: Context(App)) -> Response {
// Ensure only authenticated ones reach this route
use ctx <- middleware.apply([auth_user], ctx)
// Get the authenticated user from our context
let assert option.Some(user) = ctx.app.user
// Pass the authenticated user to your view
response.html(dashboard.render(user: user), 200)
}
---
import database/main/models/user/gen/user.{type User}
props(user: User)
---
<x-layouts:app>
<div class="max-w-2xl mx-auto py-12">
<h1 class="text-2xl font-bold">
Welcome back, {{ user.name }}
</h1>
<p class="mt-2">{{ user.email }}</p>
</div>
</x-layouts:app>
/// @get "/users/:user_id"
pub fn show(ctx: Context(App), user_id: Int) -> Response {
// Fetch from cache, or query DB on miss
let user = {
use <- cache.remember_json(
ctx.app.cache,
"user:" <> int.to_string(user_id),
3600,
user.decoder(),
user.encoder(),
)
user.find_or_fail(ctx.app.db, user_id)
}
response.html(user_show.render(user: user), 200)
}
Define routes with simple annotations in your controllers. Glimr compiles them into a pattern-matched router, not a runtime lookup table. The result is extremely fast routing with zero overhead.
Apply middleware at the controller or route level. Routes recompile automatically when you save, so there's no manual step between writing a handler and testing it.
Learn moreIntercept requests before they reach your controllers. Modify context, check auth, log requests, rate limit, or run any custom logic.
Apply per-route with the use syntax, or per-controller with a middleware() function. Changes to context flow through to all downstream handlers.
Learn moreLoom templates compile to type-safe Gleam. Inspired by Pheonix LiveView, add event handlers and they become reactive over WebSockets. No JavaScript required.
All state lives on the server. Interactions trigger a minimal diff that patches only what changed. Built-in SPA navigation, two-way binding, loading states, and components with slots.
Learn moreDefine rules declaratively and get back a typed struct, not a bag of strings. Validation errors are handled automatically: flashed to the session for HTML routes, returned as JSON for APIs.
The use syntax short-circuits on failure, so your controller only runs when data is valid. Transform and sanitize inputs in one place before they reach your business logic.
Learn moreDefine your schema in Gleam with a fluent, type-safe API. Glimr compares it against a stored snapshot, detects changes, and generates driver-specific SQL migrations automatically. Just run ./glimr db_gen and you're done.
Learn moreWrite real SQL in plain .sql files. Glimr automatically generates fully typed Gleam repository functions you can use directly in your controllers. No ORM magic, no query builders, just write plain SQL like God intended.
Need multiple databases? Glimr supports multiple connections out of the box. SQLite, PostgreSQL, or both. Each with its own models, migrations, and generated repositories.
Learn moreBuilt-in caching with file, Redis, SQLite, and PostgreSQL stores. The remember pattern fetches from cache or computes on miss in a single expression.
Cache strings, JSON, or structured data with auto-generated encoders and decoders. Supports TTL, increment/decrement for rate limiting, and flush operations.
Learn moreRun ./glimr make_auth user and get a complete auth stack: model, migrations, login/register controllers, validators, middleware, and Loom views. All generated, all type-safe.
Need separate auth for admins and users? Use scoped mode for fully independent auth stacks with their own middleware, sessions, and routes.
Learn moreMost frameworks were designed before LLM agents wrote code. Gleam and Glimr have the constraints that make an agent-driven workflow actually reliable.
Gleam's strict type system and compiler catch errors instantly. When an LLM generates wrong code, it gets precise error messages it can act on, not silent failures that surface in production.
Gleam is a functional language, meaning no inheritance, no decorators, no implicit behavior. Everything in Gleam is explicit and in the code. An LLM doesn't need context it can't see.
Glimr's opinionated conventions mean the LLM doesn't have to choose between ORMs, routers, or project structures. There's one proven path, and it's well-documented.
Frameworks like Laravel and Rails pioneered convention over configuration, but they're built on dynamic, object-oriented languages. An LLM can generate code that looks right, passes linting, and breaks in production. Gleam's compiler won't let that happen. Functions take data in and return data out. Conventions tell the LLM what to write. The type system tells it what it got wrong.
Glimr is still young, but comes with a lot out of the box. Routing, auth, validation, caching, and more. No need to assemble a stack from scattered packages.
Apply middleware per-route or per-controller. Compose auth, rate limiting, logging, and custom logic in any order.
Organize routes into web and API groups with their own middleware stacks. Add custom groups as your app grows.
Routes compile to pattern-matched Gleam code on the BEAM runtime. Zero overhead, zero runtime lookups.
First-class support for both databases. Write SQL, and Glimr generates fully typed repository functions and driver-specific migrations automatically.
Built-in caching with Redis, file, SQLite, and PostgreSQL stores. The remember pattern handles cache-or-compute in a single expression.
Run multiple database connections side by side. Each with its own models, migrations, queries, and generated repositories.
One command generates a full auth stack: model, controllers, validators, middleware, and views. Scoped mode for multi-model auth.
Server-driven reactivity over WebSockets. SPA navigation, two-way binding, loading states, and components with slots. No JS required.
Declarative rules that return typed structs. Automatic error handling for HTML and JSON responses. Sanitize inputs in one place.