Introducing Fitz: a language where HTTP, Postgres, JWT, and WebSockets are part of the syntax

rust dev.to

Fitz is a new programming language built in Rust, with a gradually-typed compiler. The pitch: instead of stacking FastAPI + SQLAlchemy + python-jose + Celery + Pydantic + uvicorn + Alembic + typer on top of Python, the things they each solve live inside the language: HTTP routing, OpenAPI/AsyncAPI generation, async/await, JWT auth, password hashing, an ORM with a pure-Rust Postgres driver, schema migrations, WebSockets, cron, background jobs, a CLI builder, healthchecks, observability with OpenTelemetry, secrets as opaque types, and a fitz deploy orchestrator. One binary. Zero external deps for the core stack. Repo: github.com/Thegreekman76/fitz · Docs: thegreekman76.github.io/fitz<

I've been building web APIs in Python for years — FastAPI plus the usual cast: SQLAlchemy, python-jose for JWT, passlib for Argon2, Celery + Redis for background jobs, Pydantic for validation, uvicorn for serving, alembic for migrations. Every API I ship needs roughly the same nine libraries, each with its own conventions, its own breaking changes, its own way to integrate with the others.

At some point I asked myself the obvious question: why isn't this just the language?

That question is Fitz.

What Fitz looks like

Let's start with the picture, then walk through the pieces.

@server(43928)
fn main() => 0

type User { id: Int, email: Str, name: Str, role: Str }
type Credentials { email: Str, password: Str }
type LoginResponse { token: Str }

let SECRET = "demo-secret-change-me-in-prod"
let ADA_HASH = hash.password("secret-ada-123")

@auth_provider
fn check_token(headers: Map<Str, Str>) -> Result<User> {
    let auth: Str = match headers.get("authorization") {
        Ok(v) => v,
        Err(_) => return Err("missing Authorization header"),
    }
    let parts = auth.split(" ")
    if (parts.len() != 2 or parts[0] != "Bearer") {
        return Err("expected 'Bearer <token>'")
    }
    let claims = jwt.decode(parts[1], SECRET)?
    return find_user(claims["email"])
}

@post("/login")
fn login(creds: Credentials) -> LoginResponse {
    let user: User = match find_user(creds.email) {
        Ok(u) => u,
        Err(_) => return 401 { "error": "invalid credentials" },
    }
    if (not hash.verify(creds.password, ADA_HASH)) {
        return 401 { "error": "invalid credentials" }
    }
    let claims = { "email": user.email, "role": user.role }
    return LoginResponse { token: jwt.encode(claims, SECRET) }
}

@authenticated
@get("/me")
fn me(user: User) -> User => user

@admin
@get("/admin/users")
fn admin_list(user: User) -> List<User> { ... }
Enter fullscreen mode Exit fullscreen mode

What this code does, without a single import or external dependency:

  • Starts an HTTP server on port 43928.
  • Auto-generates OpenAPI 3.1 at /openapi.json.
  • Auto-serves Scalar UI at /docs with a working "Authorize" button.
  • Signs and verifies JWT tokens (HS256/384/512 supported).
  • Hashes passwords with Argon2id (OWASP recommendation, not bcrypt).
  • Statically validates that every @authenticated/@admin handler has an @auth_provider declared, that the provider returns the right User type, and that @admin handlers have a role: Str field on the User.
  • Compiles to a single native binary with fitz build, with bit-for-bit parity against fitz run.

The auth, the hashing, the JWT, the OpenAPI with bearerAuth security scheme, the 401/403 responses — all of that is in the binary fitz itself. There's no requirements.txt, no package.json, no Cargo.toml for the user.

Why "first-class" matters

"First-class citizen" is one of those phrases that gets thrown around. Here's what I mean concretely.

In FastAPI, @app.get("/users") is a method on an object instance. The framework is a library you opt into. The router is a Python data structure. Authentication is a Depends(...). None of those things are visible to the type checker as anything special — they're just function calls and decorators that happen to produce metadata.

In Fitz, @get("/users") is a decorator the compiler understands. The checker validates the path template, the parameter types against the path params, the body type, the return type. The OpenAPI generator inspects the AST directly — it doesn't introspect runtime objects, it doesn't need decorators that "register" themselves. The User you return in your handler is the same User that appears in the generated schema and in the Scalar UI.

This sounds like a small distinction until you live it for a week. Then you stop fighting "why does Pydantic disagree with SQLAlchemy about whether this field is optional" and you start writing endpoints.

The pieces

HTTP + OpenAPI + Scalar UI, all auto

type Post { id: Int, title: Str, body: Str, tags: List<Str> }

@get("/posts")
fn list_posts() -> List<Post> { ... }

@post("/posts")
fn create_post(post: Post) -> Post { ... }
Enter fullscreen mode Exit fullscreen mode

That's all you need. /openapi.json and /docs (Scalar UI) appear automatically. Path params (/posts/{id}) are typed and coerced. JSON body deserialization checks for missing required fields, applies defaults, validates nullables, rejects extras. You can opt out with @server(docs=false).

WebSockets, typed, with AsyncAPI auto-generated

type ChatMessage { from: Str, text: Str }

@server(43929, ws_heartbeat_secs=30)
fn main() => 0

@authenticated
@ws("/chat")
async fn chat(conn: WsConn<ChatMessage>, user: User) {
    loop {
        let msg = match conn.recv() {
            Ok(m) => m,
            Err(_) => break,
        }
        conn.broadcast(ChatMessage { from: user.name, text: msg.text })
    }
}
Enter fullscreen mode Exit fullscreen mode

Every frame is auto-marshalled to and from the declared type. Auth runs before the WebSocket upgrade — invalid token gets a 401 without ever opening the socket. Ping/pong heartbeat keeps the connection alive past Nginx's 60s default. /asyncapi.json is generated automatically (the event-driven sibling of OpenAPI). I don't know of another language that auto-generates AsyncAPI from typed source.

Background jobs and cron, no Redis required

@cron("*/5 * * * *")
async fn cleanup_old_sessions() {
    db.exec("DELETE FROM sessions WHERE expires_at < now()")
}

@background
async fn send_welcome_email(email: Str) {
    // expensive thing
}

@post("/signup")
fn signup(creds: Credentials) -> User {
    let user = create_user(creds)
    spawn(send_welcome_email(user.email))  // fire-and-forget, typed Future<Null>
    return user
}
Enter fullscreen mode Exit fullscreen mode

No Celery. No Redis. No celery worker -A app next to your uvicorn process. The scheduler is in your binary. Suitable for 90% of services — when you outgrow it, you outgrow it for a reason, and that's a Fase 11+ problem.

A native ORM with a pure-Rust Postgres driver

This is the piece I'm most proud of, and the one that took the longest. Fitz has its own Postgres driver written in Rust — no libpq, no tokio-postgres, no sqlx. The wire protocol (v3.0), SCRAM-SHA-256 auth, prepared statements, the binary format for 11 OID types — all implemented from the RFC.

@table("users")
type User {
    @primary id: Int,
    email: Str,
    name: Str,
    @has_many("Post", "user_id") posts: List<Post>,
}

@table("posts")
type Post {
    @primary id: Int,
    user_id: Int,
    title: Str,
    body: Str,
    @belongs_to user: User?,
}

@get("/users")
async fn list_users(db: DbConn) -> List<User> {
    return User.all(db).preload("posts").await
}

@get("/users/{id}")
async fn get_user(db: DbConn, id: Int) -> Result<User> {
    return User.where(fn(u) => u.id == id).first(db).await
}

@post("/users")
async fn create_user(db: DbConn, user: User) -> User {
    return User.insert(db, user).await
}
Enter fullscreen mode Exit fullscreen mode

The closure inside .where(...) is translated to parametrized SQL at compile timefn(u) => u.id == id becomes WHERE id = $1. Operators like .is_in([...]), .like(...), .ilike(...), .contains(...), plus JSONB operators like .has_key(...), .contains_json(...) all map to native Postgres operators. Eager loading with .preload("posts") issues a single batched query. Aggregates (.sum/.avg/.min/.max/.count) and GROUP BY are supported through a separate Aggregated<Row> type.

This compiles to native code via fitz build. The generated binary makes the same Postgres calls. Zero overhead at runtime for the SQL — it's already constant by the time the binary runs, comparable in performance to Diesel or sqlx.

Python interop when you do need it

from python import math, json

let radius = 5.0
let area: Float = math.pi * radius * radius

let parsed: Result<Map<Str, Any>> = match json.loads("{\"name\": \"ada\"}") {
    Ok(d) => Ok(d),
    Err(e) => Err("malformed JSON: {e}"),
}
Enter fullscreen mode Exit fullscreen mode

SQLAlchemy, NumPy, pandas, anything on PyPI — accessible from Fitz with from python import .... The runtime embeds CPython via PyO3. Python exceptions become Result::Err automatically. Async Python (asyncpg, SQLAlchemy 2.x async) bridges to Fitz's .await transparently. You can even do fitz build --bundle-python to ship a binary with CPython embedded — no Python required on the destination machine.

This is intentional. Fitz isn't trying to replace Python's ecosystem — it's trying to give you a better language for the web layer while keeping the door open to everything Python has already built.

Async, finally without color

async fn fetch_user(id: Int) -> Result<User> { ... }

async fn main() {
    let user = fetch_user(42).await?
    print("got {user.name}")
}
Enter fullscreen mode Exit fullscreen mode

async/await is in the core, on a tokio runtime. The ? operator works through Result<T>. The type checker enforces that ? only appears inside functions that return Result<...>. Compiles to async fn + .await in Rust — same execution model as Rust async, same multi-threaded executor.

CLI builder — same language, command-line tools

Fitz isn't only for HTTP services. The same compiler ships a built-in CLI builder, no library needed:

@command("greet", desc="Greet a person")
fn greet(name: Str, loud: Bool = false, count: Int = 1) -> Int {
    let n = count
    while n > 0 {
        if loud { print("HELLO, {name}!") } else { print("hello, {name}") }
        n = n - 1
    }
    return 0
}

@command("add", desc="Sum two numbers")
fn add(a: Int, b: Int) -> Int {
    print("{a + b}")
    return 0
}
Enter fullscreen mode Exit fullscreen mode
$ ./mybin greet Ada --loud --count 3
HELLO, Ada!
HELLO, Ada!
HELLO, Ada!

$ ./mybin --help
USAGE: mybin <command> [ARGS] [OPTIONS]
COMMANDS:
    greet    Greet a person
    add      Sum two numbers
Enter fullscreen mode Exit fullscreen mode

Convention over decoration: params without defaults are positional args, params with defaults are flags. Bool with default = false becomes --flag, other types become --flag <value>. Short flags auto-derive (--loud-l) with conflict detection. Help auto-generated, exit codes POSIX standard. Bit-for-bit parity between fitz run (development) and fitz build (a self-contained binary you can drop into /usr/local/bin).

This is the same language. Same type checker. Same async/await. Same Result<T> for errors. If your tool needs to hit the database, the ORM is there. If it needs HTTP, @get/@post are there. The line between "web service" and "CLI tool" stops being a stack decision.

Production-ready stack — from repo to production

This is what separates Fitz from "interesting prototype" languages. Real services need health checks, secrets, observability, and a way to ship. All of them are part of the language:

@server(43928)
fn main() => 0

// Auto-mounted at GET /healthz and /readyz — Kubernetes-friendly.
@healthz
fn liveness() -> Bool => true

@readyz
async fn readiness(db: DbConn) -> Bool {
    return match db.exec("SELECT 1").await {
        Ok(_) => true,
        Err(_) => false,
    }
}

// Secret<T> never leaks to logs, prints "***" on Display.
let db_url: Secret<Str> = secret("DATABASE_URL")
let log_level: Str = config("LOG_LEVEL", "info")

// Tracing + metrics with one decorator each.
@trace(name="process_order")
@metric(name="orders")
async fn process(order: Order) -> Result<Receipt> {
    // process_order_duration_seconds (histogram) and orders_calls_total
    // (counter) populate automatically on drop.
}

// Feature flags with two sources: fitz.toml [flags] + FITZ_FLAG_<NAME> env vars.
@flag("new-checkout")
@post("/v2/checkout")
fn v2_checkout(body: Cart) -> Receipt { ... }
Enter fullscreen mode Exit fullscreen mode

Behind the scenes:

  • HTTP access logs auto-emit with trace_id/span_id propagated to every log.info(...) inside the handler.
  • OpenTelemetry OTLP export with one env var: OTEL_EXPORTER_OTLP_ENDPOINT. Spans flow to Jaeger/Tempo/Honeycomb. Without the env var, zero overhead, zero network calls.
  • Prometheus /metrics endpoint exposes counters and histograms — @server(prometheus=true) enables.
  • @flag on HTTP/WS handlers returns 404 when the flag is off — gate the hot path before middleware/auth.

Deploying:

# Generate the Dockerfile + docker-compose.yml from the program shape.
fitz docker init

# Build the binary, the Docker image, push to a registry.
fitz deploy docker --tag mycorp/api:v1

# Or bring up locally with compose.
fitz deploy compose
Enter fullscreen mode Exit fullscreen mode

fitz docker init reads your AST. If there's a db.connect(...), it adds Postgres to the compose. If there's @server(N), it sets EXPOSE N. If there's @cron, it adds restart: unless-stopped. If there's from python import ..., it picks python:3.12-slim-bookworm instead of distroless. It generates what you'd write by hand, you commit it, edit when you need to.

I'm not aware of another language where deployment is a language feature. It is here because every project I shipped in Python ended with two days of debugging Dockerfile gotchas.

What's the tooling like?

This is the part I underestimated when I started. A language without good tools is dead on arrival. Here's the current state:

  • fitz run — interpret the file directly. Fastest feedback loop.
  • fitz build — compile to a native binary via a generated Rust project. Bit-for-bit parity with fitz run is a hard requirement.
  • fitz check — type checker only, no execution.
  • fitz test — built-in test runner with @test decorator and assert, assert_eq, assert_throws. Cargo-style output.
  • fitz dev — hot reload. Watches *.fitz and fitz.toml, kills and respawns the child on change.
  • fitz fmt — opinionated formatter, zero config. Preserves your comments and blank lines.
  • fitz lint — 4 built-in lints with // @allow(<name>) suppression. Cargo-clippy-style output.
  • fitz repl — interactive REPL with multi-line support, :type, :load, persistent history.
  • fitz openapi — emit the OpenAPI schema without running the server.
  • fitz db diff/migrate — schema migration tooling. Diff the live DB against the @table types in your code, generate idempotent migrations, apply them with fitz db migrate. Same model as Alembic but with the types as source of truth.
  • fitz docker init/build — generate the Dockerfile + .dockerignore + docker-compose.yml from the program shape, then docker build wrapped.
  • fitz deploy docker/compose — thin wrapper to ship the image or bring up locally with one command.
  • VSCode extension — diagnostics + hover + go-to-definition + autocomplete + signature help + format on save + bidirectional type inference for callbacks, multi-platform distribution.
  • fitz new + fitz add + fitz remove + fitz update — package manager with fitz.toml, lockfile, path deps, git deps.

The LSP is real (tower-lsp under the hood). The formatter is real (your code round-trips through it). The test runner is real. The whole thing is dogfooded — I write Fitz code with the same VSCode extension I ship.

Being honest about state

This is a one-developer project. I started learning Rust to build it. I'm not going to pretend it's production-ready for everyone — here's what's true today (June 2026, release v0.15.0):

What works end-to-end, with bit-for-bit fitz runfitz build parity:

  • HTTP server with @get/@post/@put/@delete, OpenAPI auto, Scalar UI.
  • Middleware chain with @middleware(fn) + CORS built-in.
  • JWT auth with @auth_provider/@authenticated/@admin + @requires("custom_role") for RBAC. Argon2id password hashing. Token blacklist over Postgres for logout/refresh.
  • WebSockets with WsConn<T>, AsyncAPI auto, heartbeat, auth pre-upgrade.
  • Cron jobs with @cron("expr") (with retry, timezone, persistence, catch-up), background jobs with @background + spawn(...).
  • Postgres ORM with @table/@primary/@column/@belongs_to/@has_many, closure-to-SQL, eager loading, transactions (db.transaction(fn)), schema migrations (fitz db diff/migrate).
  • TLS strict for Postgres (sslmode=require).
  • Async/await on tokio.
  • Python interop with from python import ..., including auto-bridging async.
  • CLI builder with @command — same language for CLI tools.
  • Production stack: @healthz/@readyz, Secret<T>, secret()/config(), @trace/@metric, @flag, OpenTelemetry OTLP export, Prometheus /metrics, fitz docker init/build, fitz deploy.
  • Package manager with path deps and git deps.
  • Full tooling: LSP (with signature help, format on save, hover over params and bindings), fmt, test, dev, repl, lint.

What's not in the box yet:

  • Frontend in .fitz (single-file components, SSR). Roadmap (Fase 11) — the most ambitious bet of the project. Not started.
  • A public package registry. Path deps and git deps work today; the registry is on hold until there's real demand.
  • fitz deploy targets beyond docker/compose (no fly/railway/k8s wrapper yet — use the native CLIs).
  • Interactive debugging in VSCode (Debug Adapter Protocol). Workarounds: print, REPL :type/:env, LSP diagnostics. Tracked as V6 in the backlog.

What's stable: ~3030 Rust unit tests + 13 LSP E2E + 360 compile E2E (smoke over every example in the guide) + ~140 more across other suites running in CI on every push. Clippy -D warnings clean.

How to try it

# Install on Linux / macOS / WSL
curl -sSf https://thegreekman76.github.io/fitz/install.sh | sh

# Install on Windows (PowerShell)
irm https://thegreekman76.github.io/fitz/install.ps1 | iex

# Or grab a release binary from GitHub
# https://github.com/Thegreekman76/fitz/releases

# Reopen the terminal so the PATH change takes effect, then:
fitz --version
Enter fullscreen mode Exit fullscreen mode

VSCode extension (recommended — syntax highlighting, hover with types, autocomplete, signature help, format on save): grab the .vsix for your platform from the same releases page (fitz-lang-<platform>.vsix) and install it with code --install-extension fitz-lang-<platform>.vsix --force. The Language Server is bundled inside — no separate install needed. Reload VSCode once.

First server:

fitz new my-api --http
cd my-api
fitz dev
Enter fullscreen mode Exit fullscreen mode

Eight boilerplates ship in the repo under boilerplates/:

  • api-simple — minimal HTTP API.
  • api-middleware-cors — middleware chain + CORS configuration.
  • api-postgres-fitz — ORM + Postgres, Dockerized.
  • api-postgres-python — Postgres via Python/SQLAlchemy interop.
  • api-websocket — typed WebSocket chat.
  • api-orm-full — the full showcase: auth + ORM + WebSockets + cron + jobs.
  • api-fullstack-postgres — backend + minimal frontend in one binary.
  • cli-tool — CLI app with @command (no HTTP).

Each one runs with docker compose up or fitz dev. The README has the full matrix.

Why I built this

I live in El Chaltén, in Argentine Patagonia. The Fitz Roy is the granite tower that defines the skyline here. Borges wrote that we live in a country where the past is uncertain and only the future is real. I think that's true of programming languages too: the past is full of accumulated workarounds for missing language features, and the future is whatever you decide to build.

I've spent ten years writing API code in Python. I love FastAPI. But every time I start a new project, the first three hours are spent gluing libraries together to do the same thing I did last week. At some point the question becomes: what would a language look like that started from this set of needs in 2026, instead of growing them as patches on a language designed for shell scripting in 1991?

That's Fitz.

It's not done. I'm one person. It will get there.

Repo: github.com/Thegreekman76/fitz
Docs and course: thegreekman76.github.io/fitz
Guide (34 chapters): thegreekman76.github.io/fitz/guide/
Roadmap: docs/roadmap.md
CHANGELOG: CHANGELOG.md — every release with detail.
Issues: github.com/Thegreekman76/fitz/issues

If you try it, I want to hear what broke. Open an issue or a discussion on GitHub.

Source: dev.to

arrow_back Back to Tutorials