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.
// 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: "[email protected]",
}
}
// 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.
// 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.
// 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.
// 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()