Routes & URLs
One annotation per handler defines everything — HTTP method, URL, OpenAPI responses, tags. Zero route registration at the app level.
The #[route] Macro
Define a handler with a single annotation. Routes are auto-discovered at startup.
use floz::prelude::*;
#[route(
post: "/users",
tag: "Users",
desc: "Create a new user",
auth: jwt,
rate: "100/min",
resps: [
(201, "User created"),
(400, "Validation failed"),
(401, "Unauthorized"),
],
)]
async fn create_user(
body: web::types::Json<CreateUser>,
db: web::types::State<Db>,
) -> HttpResponse {
// ...
}
No route registration needed. Every #[route] handler auto-registers itself. In main.rs, just call App::new().run().await.
Attribute Fields
| Field | Required | Example | Purpose |
|---|---|---|---|
get: / post: / put: / patch: / delete: | ✓ | get: "/users" | HTTP method + path |
tag: | tag: "Users" | Logical grouping (OpenAPI + CLI) | |
desc: | desc: "List all users" | Description for docs | |
resps: | [(200, "OK")] | Response status + description | |
auth: | auth: jwt | Auth requirement (jwt, api_key, none) | |
rate: | rate: "100/min" | Per-endpoint rate limit |
GET — List Resources
#[route(
get: "/users",
tag: "Users",
desc: "List all users",
resps: [(200, "Users listed")],
)]
async fn list_users(db: web::types::State<Db>) -> HttpResponse {
let users = User::all(&db).await.unwrap();
res!(pp!(&users).unwrap_or_default())
}
POST — Create Resource
#[route(
post: "/users",
tag: "Users",
desc: "Create a new user",
resps: [
(201, "User created"),
(400, "Validation failed"),
],
)]
async fn create_user(
body: web::types::Json<CreateUserPayload>,
db: web::types::State<Db>,
) -> HttpResponse {
let user = User::create()
.name(&body.name)
.email(Some(body.email.clone()))
.execute(&db).await.unwrap();
res!(pp!(&user).unwrap_or_default(), 201)
}
Path Parameters
Use :param syntax — auto-translated to ntex's {param} internally:
#[route(
get: "/users/:id",
tag: "Users",
resps: [
(200, "User found"),
(404, "User not found"),
],
)]
async fn get_user(
path: web::types::Path<i32>,
db: web::types::State<Db>,
) -> HttpResponse {
let id = path.into_inner();
match User::get(id, &db).await {
Ok(user) => res!(pp!(&user).unwrap_or_default()),
Err(_) => JsonResponse::not_found("User not found"),
}
}
// Multiple path params
#[route(get: "/posts/:post_id/comments/:id")]
async fn get_comment(
path: web::types::Path<(i32, i32)>,
) -> HttpResponse {
let (post_id, comment_id) = path.into_inner();
// ...
}
Query Parameters
// GET /users?limit=20&offset=0&search=alice
#[route(
get: "/users",
tag: "Users",
desc: "Search users with pagination",
)]
async fn search_users(
params: web::types::Query<PaginationParams>,
db: web::types::State<Db>,
) -> HttpResponse {
let p = params.into_inner();
// p.limit, p.offset, p.search, p.order_by, p.filter
}
JSON Request Body
#[derive(Deserialize)]
pub struct CreateUserPayload {
pub name: String,
pub email: String,
}
#[route(
post: "/users",
tag: "Users",
resps: [(201, "Created"), (400, "Bad request")],
)]
async fn create(body: web::types::Json<CreateUserPayload>) -> HttpResponse {
info!("Creating user: {}", body.name);
// body.name, body.email available directly
}
Auto-Discovery
All #[route] handlers are collected at compile time and registered automatically when the app starts. No manual wiring needed:
#[ntex::main]
async fn main() -> std::io::Result<()> {
App::new().run().await // auto-discovers all #[route] handlers
}
On startup, floz logs all discovered routes and prints a route table in dev mode:
🚀 floz starting...
Environment: development
Database pool initialized
Auto-discovered 5 route(s)
METHOD PATH TAG DESCRIPTION
────── ──────────────────────────────── ──────────────────── ───────────────────
GET /auth/login Auth: Session Serve the login page
POST /users Users Create a new user
GET /users Users List all users
GET /users/:id Users Get user by ID
DELETE /users/:id Users Delete a user
Full REST Example
// src/app/user/route.rs
use floz::prelude::*;
use super::model::*;
#[route(get: "/users", tag: "Users", resps: [(200, "OK")])]
pub async fn list(db: web::types::State<Db>) -> HttpResponse {
match User::all(&db).await {
Ok(users) => JsonResponse::ok(&users),
Err(e) => JsonResponse::error(&e.to_string()),
}
}
#[route(
get: "/users/:id",
tag: "Users",
resps: [(200, "Found"), (404, "Not found")],
)]
pub async fn show(
path: web::types::Path<i32>,
db: web::types::State<Db>,
) -> HttpResponse {
match User::get(path.into_inner(), &db).await {
Ok(user) => JsonResponse::ok(&user),
Err(_) => JsonResponse::not_found("User not found"),
}
}
#[route(
delete: "/users/:id",
tag: "Users",
resps: [(204, "Deleted"), (404, "Not found")],
)]
pub async fn destroy(
path: web::types::Path<i32>,
db: web::types::State<Db>,
) -> HttpResponse {
match User::get(path.into_inner(), &db).await {
Ok(user) => {
user.delete(&db).await.unwrap();
JsonResponse::no_content()
}
Err(_) => JsonResponse::not_found("User not found"),
}
}
Convention: Domain modules follow the src/app/{module}/ pattern with model.rs (Sleek schema), route.rs (handlers), and mod.rs. Use floz generate scaffold to generate this structure automatically.
OpenAPI & Swagger UI
Sleek auto-generates OpenAPI (Swagger) Documentation dynamically at runtime. Say goodbye to bloated derive macros and duplicate routing configurations.
Because floz already captures all the information using the single #[route(...)] macro (methods, paths, tags, descriptions, responses), it translates this metadata natively into an OpenAPI spec when the server boots.
Zero-Configuration Endpoints
Without writing any extra code, your App::new().run().await automatically provisions two endpoints:
| Endpoint | Format | Description |
|---|---|---|
GET /api-docs/openapi.json | application/json | The raw OpenAPI 3.1.0 specification generated from registered handlers |
GET /docs | text/html | A fully functional, embedded Swagger UI frontend to explore your API |
Path Parameter Translation
Express-style path parameters natively defined in the route (e.g. :id) will automatically be converted to the OpenAPI path specification format {id} in the generated Swagger UI.
Note: Currently the generated OpenAPI schema maps tags, status codes, and descriptions. In a future update, structured JSON bodies mapping to database definitions will be fully represented.
Precise JSON Schema Typing
By default, responses output the description string. You can bind Utoipa Schema implementations directly to the response to produce strict JSON schema structures automatically using Json<T> syntax.
#[derive(serde::Serialize, utoipa::ToSchema)]
pub struct User {
#[schema(example = "1")]
pub id: i32,
#[schema(example = "Alice")]
pub name: String,
}
#[route(
get: "/users",
tag: "Users",
resps: [(200, "Returns User list", Json<User>)]
)]
async fn get_users() -> web::HttpResponse {
// ...
}