Skip to content

HTTP Routes and API Quality

@get("/path") and @post("/path") register functions as HTTP handlers. The runtime builds the route tree at load time; http.serve(port) starts the server. Route parameters are available via req.params.<name>:

Index, health, route with parameter, and POST endpoint with a 201 response.

10-http-routes.zolo
Playground
// Feature: `@get` / `@post` — registers an HTTP route handler

// Syntax: `@get("/path")`, `@post("/path")` before a `fn`. The runtime

// builds the route tree at load time.

// When to use: declare a REST API inline, without a separate router builder.

//

// This file only DEFINES routes; `http.serve(<port>)` was omitted so

// that `zolo check` can run without spinning up a server. In production,

// uncomment the last line.


use std::http
use std::json

@get("/")
fn index() {
  return "Hello from decorator routes!"
}

@get("/health")
fn health() {
  return #{status: "ok"}
}

// Path params arrive in `req.params.<name>`.

@get("/users/:id")
fn get_user(req) {
  return #{
    id: req.params.id,
    name: "User",
    email: "user@example.com",
  }
}

// POST receives JSON and returns 201.

@post("/users")
fn create_user(req) {
  let body = req.json()
  return http.response(201, json.encode(body))
}

// Multiple routes in the same module are all registered.

@get("/time")
fn get_time() {
  return #{ts: 0}
}

print("routes registered")
// expected: routes registered


// To actually start the server:

// http.serve(3001)

@deprecated("message") emits a runtime warning when the function is called, but does not prevent execution. Use it to maintain compatibility with existing code while migrating to a new API:

Old function marked as deprecated; replacement available in parallel.

03-deprecated.zolo
Playground
// Feature: `@deprecated(reason)` — marks a function as obsolete
// Syntax: `@deprecated("message")`. Emits a warning when called.
// When to use: keep backward compatibility while migrating users to
// a new API.

@deprecated("Use greet_v2() — returns formatted string with locale.")
fn greet_old() -> str {
  return "Hello!"
}

fn greet_v2() -> str {
  return "Hello, World!"
}

// The call still works; the warning shows up at runtime.
print(greet_old())

// expected: Hello!

print(greet_v2())

// expected: Hello, World!

// Useful to point at the replacement explicitly.
@deprecated("use compute_v2(x, y) instead")
fn compute_old(x: int) -> int {
  return x * 2
}

fn compute_v2(x: int, y: int) -> int {
  return x * y
}

print(compute_old(5))
// expected: 10
print(compute_v2(5, 3))
// expected: 15

@must_use warns when a function's, struct's, or method's return value is silently discarded — a common pattern with Result, chained builders, and resource handles. Use _ = expr to signal intentional discard:

@must_use on a free function, a struct, and a builder method.

13-must-use.zolo
Playground
// Feature: @must_use — warn when a returned value is discarded

// Syntax: `@must_use` or `@must_use("custom reason")` on `fn`, `impl`

//   methods, `struct`, or `newtype`.

// When to use: APIs where ignoring the return value is almost always a

// bug — `Result`, `Option`, file handles, lock guards, builders that

// require `.build()`, fluent APIs.

//

// The check is currently a LINT (warning), not a hard type error. Use

// `_ = expr` to silence intentionally; see

// `examples/features/02-variables/16-phony-discard.zolo`.

//

// See `specs/must-use-attribute.md` for the full specification.


// 1. @must_use on a free function -----------------------------------


@must_use("the divide result must be handled — division can fail")
fn divide(a: int, b: int) -> int {
  if b == 0 { return -1 }
  return a / b
}

// Consume properly:

let r = divide(10, 2)
print(r)

// Discard explicitly via phony assignment:


// Discarding without `_ =` triggers `must-use` lint warning:

//

// divide(10, 3)

//   warning[must-use]: the divide result must be handled — division can fail


// 2. @must_use on a struct (every constructor / fn returning it warns)


_ = divide(10, 5)

@must_use("Transaction must be committed or rolled back")
struct Transaction {
  id: int,
}

impl Transaction {
  fn open(id: int) -> Transaction {
    return Transaction { id: id }
  }

  fn commit(self) -> bool {
    return true
  }
}

// `Transaction.open(1)` returns a @must_use struct — descartar avisa:

//

// Transaction.open(99)

//   warning[must-use]: Transaction must be committed or rolled back


let tx = Transaction.open(1)
let _ok = tx.commit()

// 3. @must_use on a newtype -----------------------------------------

// Same mechanism as struct — the type is registered, any function that

// returns it (or constructor call) gets flagged on discard.

//

//   @must_use("ParsedConfig must be passed to apply_config()")

//   newtype ParsedConfig(str)

//

//   parse_config("foo")    // would warn — return value discarded


// 4. @must_use on an impl method ------------------------------------


struct Builder {
  name: str,
}

impl Builder {
  fn new() -> Builder {
    return Builder { name: "default" }
  }

  @must_use("Builder.named() returns a new Builder — use it or assign it")
  fn named(self, name: str) -> Builder {
    return Builder { name: name }
  }
}

let b = Builder.new()
let b2 = b.named("custom")
print(b2.name)

// Discarding `.named(...)` would trigger must-use warning.


print("must_use demo complete")

@diagnostic(severity, rule) adjusts the severity of a lint within a specific scope: off suppresses it, warn keeps it as a warning, error promotes it to a compile-time error. Inner scopes override outer ones:

Local suppression, promotion to error in critical code, and inner scope overriding outer.

14-diagnostic.zolo
Playground
// Feature: @diagnostic(severity, rule) — scoped lint severity override
// Syntax: `@diagnostic(off|warn|error, rule_name)` on `fn`, `struct`,
//   `impl method`. Multiple decorators stack; innermost scope wins.
// When to use:
//   - silence a lint locally (`off`) without polluting global config;
//   - promote a warning to a hard error (`error`) in critical code;
//   - downgrade an error to a warning (`warn`) during migration.
//
// Rule names use underscores in source — `unused_variable`, `must_use`,
// etc. They map to the same names the linter emits.
//
// See `specs/diagnostic-attribute.md` for the full specification.

@must_use("the result must be handled")
fn produce() -> int {
  return 42
}

// 1. `off` — suppress the lint locally -----------------------------
//   Same effect as the legacy `@allow(unused_variable)` decorator.
@diagnostic(off, unused_variable)
fn experiment() {
  let placeholder = compute()
  print("still deciding what to do with placeholder")
}

fn compute() -> int {
  return 100
}

// 2. `error` — promote warnings to build errors --------------------
//   Use in critical code where a warning would otherwise be ignored.
//
//   @diagnostic(error, must_use)
//   fn critical_path() {
//     produce()   // would now fail the build (not just warn)
//   }

// 3. `warn` — keep noisy migrations as warnings only ---------------
//   Same severity as default in most cases, but explicit declaration
//   documents intent and survives future config changes.
@diagnostic(warn, must_use)
fn legacy_path() {
  // Result of `produce()` ignored — emits a single warning, which
  // we'll address in the next refactor.
  // already silenced via phony; @diagnostic here is
  // documentation of policy more than enforcement.
  _ = produce()
}

// 4. Innermost wins ------------------------------------------------
//   Lint severity is resolved by walking enclosing scopes outward
//   and taking the first match. Inner declarations override outer.
//   With top-level fns this means: a method inside an `impl` block
//   can opt back into a lint that the surrounding struct silenced.

@diagnostic(off, unused_variable)
struct Outer {
  name: str,
}

impl Outer {
  // Inner method opts back in to the unused-variable lint via warn.
  // (Today's parser doesn't support nested fns; impl methods are the
  // natural per-scope opt-out path.)
  @diagnostic(warn, unused_variable)
  fn build() -> Outer {
    let strict_unused = 1
    return Outer { name: "demo" }
  }
}

let outer = Outer.build()
print(outer.name)

experiment()
legacy_path()
enespt-br