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

FieldRequiredExamplePurpose
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: jwtAuth 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:

EndpointFormatDescription
GET /api-docs/openapi.jsonapplication/jsonThe raw OpenAPI 3.1.0 specification generated from registered handlers
GET /docstext/htmlA 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 { 
    // ... 
}