Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Core Concepts

Handler Registration

The #[handler] proc macro generates the following at compile time:

  1. The original function unchanged
  2. HandlerMeta β€” name, description, parameter list, return type metadata
  3. HandlerInvoker trait impl β€” type-erased invoker, deserializes params, calls the function
  4. A static invoker instance β€” referenced by the register! macro
#![allow(unused)]
fn main() {
#[handler(desc("Get user"), name("get_user"))]
async fn get_user_handler(
    state: State<AppState>,
    auth: Custom<Auth>,
    req: Data<UserIdRequest>,
) -> Result<UserInfo> {
    // ...
}
}
  • desc("...") β€” Sets description used in docs and JSDoc comments
  • name("...") β€” Overrides the client-side method name (defaults to the Rust function name)
  • cache(seconds) β€” Enables client-side caching

Multiple States

AFast supports registering multiple State types. StateMap uses TypeId as keys, with one value per type:

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct DbConfig { url: String }
#[derive(Clone)]
struct RedisConfig { url: String }
#[derive(Clone)]
struct AppConfig { name: String }

let app = AFast::new()
    .state(DbConfig { url: "postgres://...".into() })
    .state(RedisConfig { url: "redis://...".into() })
    .state(AppConfig { name: "my-app".into() });

#[handler(desc("Multiple State example"))]
async fn my_handler(
    db: State<DbConfig>,
    redis: State<RedisConfig>,
    config: State<AppConfig>,
) -> Result<()> {
    println!("DB: {}, Redis: {}, App: {}", db.url, redis.url, config.name);
    Ok(())
}
}

If a handler references a State type that was not registered, it returns a CODE_STATE_NOT_FOUND error at runtime.

Multiple Data Params

A handler can accept multiple Data<T> parameters, deserialized sequentially from the binary payload:

#![allow(unused)]
fn main() {
#[derive(AFastDeserialize, Tag)]
#[tag("Pagination")]
struct PageRequest { page: i64, size: i64 }

#[derive(AFastDeserialize, Tag)]
#[tag("Filter")]
struct FilterRequest { keyword: String, status: i32 }

#[handler(desc("Search users"))]
async fn search_users(
    page: Data<PageRequest>,
    filter: Data<FilterRequest>,
) -> Result<PageResponse> {
    // ...
}
}

Generated TypeScript client method signature:

async searchUsers(page: PageRequest, filter: FilterRequest): Promise<PageResponse>

Extractor Types

ExtractorDescriptionProtocols
State<T>Injects shared state by type from StateMap (T: Clone)All
Data<T>Deserializes request body from binary payloadHTTP/WS/TCP
Custom<T>Deserializes client-side custom context (e.g., auth token)HTTP/WS/TCP
ReceiverReceives binary messages from the client (long connection)WS/TCP
SenderSends binary messages to the client (long connection)WS/TCP
Query<T>Deserializes from URL query string (requires ordinary-http)HTTP
Param<T>Deserializes from route path params (:id) (requires ordinary-http)HTTP
Body<T>Deserializes from HTTP JSON body (requires ordinary-http)HTTP
Header<T>Deserializes from HTTP request headers (requires ordinary-http)HTTP

Services and Nesting

The service! macro builds handler trees with group for namespacing:

#![allow(unused)]
fn main() {
let api_svc = service!("api", "User API" => {
    h(health),
    group("user" => {
        h(list_users),
        h(get_user),
        group("posts" => {
            h(list_posts),
        }),
    }),
    group("chat" => {
        h(chat),  // Persistent connection using Receiver/Sender
    }),
});
}

Client namespace paths become api.user.list_users, api.chat.chat, etc.

Binary and ordinary HTTP routes can be mixed within a group:

#![allow(unused)]
fn main() {
group("user" => {
    h(get_user),                 // binary handler
    get(":id", get_user_by_id),  // GET /user/:id
    post("", create_user),       // POST /user
    delete(":id", delete_user),  // DELETE /user/:id
}),
}

Service Merge on Duplicate Name

Registering multiple services with the same name automatically merges the later handlers and routes into the first service:

#![allow(unused)]
fn main() {
let user_svc = service!("api", "User API" => {
    h(list_users),
    h(create_user),
});

let user_extra_svc = service!("api" => {
    h(delete_user),
    get(":id", get_user_http),
});

let app = AFast::new()
    .service(user_svc)
    .service(user_extra_svc);  // merge into "api"
}

Empty-Name Service

A service with an empty string name ("") registers handlers callable via binary protocol, but excluded from client code generation and API documentation:

#![allow(unused)]
fn main() {
let internal_svc = service!("", "Internal" => {
    h(debug_info),
    get("ping", ping),
});
}

Type Tags

#[derive(Tag)] generates runtime type metadata for structs and enums. The code generator recursively discovers nested types through FieldMeta.structure function pointers:

#![allow(unused)]
fn main() {
use afast::Tag;

#[derive(Tag)]
#[tag("User role")]
enum Role {
    Admin,
    User { level: i32 },
    Guest { expires_at: i64 },
    Custom(String),
}

#[derive(Tag)]
#[tag("User info")]
struct User {
    name: String,
    role: Role,           // Auto-discovers Role fields recursively
    tags: Vec<String>,    // Vec element type auto-expanded
    avatar: Option<Vec<u8>>,
}
}

Validation Rules

RuleExampleDescription
gt(value, code, "msg")#[afast(gt(0, 400, "must > 0"))]Greater than
gte(value, code, "msg")#[afast(gte(1, 400, "must >= 1"))]Greater or equal
lt(value, code, "msg")#[afast(lt(100, 400, "must < 100"))]Less than
lte(value, code, "msg")#[afast(lte(99, 400, "must <= 99"))]Less or equal
len(min, max, code, "msg")#[afast(len(1, 20, 400, "len 1-20"))]Length constraint
of(["a","b"], code, "msg")#[afast(of(["a","b"], 400, "a or b"))]Enum of values