AFast Development Guide for AI
This document is specifically designed for AI assistants to understand how to develop with the AFast framework effectively.
Core Rules
- Always use
#[handler]macro β Never manually register routes - Use
Result<T>for error handling β All handlers must returnResult<T> - Use
State<T>for shared state β Zero-copy, no clone needed - Use
Data<T>for request body β Auto deserialization from binary payload - Use
Custom<T>for auth in binary protocol β Client-provided authentication - Use
Header<T>for auth in HTTP routes β HTTP request header extraction - Use
Ctx<T>for request context β Set by hooks, read by handlers - All types must derive
Tagβ Request/response types need#[derive(Tag)]and#[tag("desc")] - Parameters must use destructuring syntax β
afast::X(name): afast::X<Type>
Handler Signature Pattern
Important: Parameters MUST use destructuring syntax afast::X(name): afast::X<Type>
Binary Protocol Handler (Basic)
#![allow(unused)]
fn main() {
use afast::{AFastDeserialize, AFastSerialize, Tag, handler};
use crate::state::AppState;
#[derive(AFastDeserialize, Tag)]
#[tag("Request body description")]
pub struct MyRequest {
#[tag("Field description")]
pub name: String,
}
#[derive(AFastSerialize, Tag)]
#[tag("Response body description")]
pub struct MyResponse {
#[tag("Field description")]
pub message: String,
}
#[handler(desc("Describe what this handler does"))]
pub async fn my_handler(
afast::State(state): afast::State<AppState>,
afast::Data(req): afast::Data<MyRequest>,
) -> afast::Result<MyResponse> {
let db = state.db.lock().await;
Ok(MyResponse { message: format!("Hello, {}!", req.name) })
}
}
Binary Handler with Authentication (Custom)
#![allow(unused)]
fn main() {
use afast::{AFastDeserialize, AFastSerialize, Tag, handler};
#[derive(AFastDeserialize, Tag)]
#[tag("Authentication token")]
pub struct AuthCustom {
#[tag("Bearer token")]
pub token: String,
}
#[handler(desc("Protected endpoint"))]
pub async fn protected(
afast::State(state): afast::State<AppState>,
afast::Custom(auth): afast::Custom<AuthCustom>,
afast::Data(req): afast::Data<MyRequest>,
) -> afast::Result<MyResponse> {
// auth.token is available
Ok(MyResponse { message: "Authorized".into() })
}
}
Binary Handler with Request Context (Ctx)
#![allow(unused)]
fn main() {
use afast::{Tag, handler};
#[derive(Clone, Debug)]
pub struct RequestInfo {
pub request_id: String,
}
#[handler(desc("Endpoint with context"))]
pub async fn with_context(
afast::Ctx(ctx): afast::Ctx<RequestInfo>,
afast::State(state): afast::State<AppState>,
) -> afast::Result<MyResponse> {
println!("Request ID: {}", ctx.request_id);
Ok(MyResponse { message: "Done".into() })
}
}
HTTP REST Handler (Ordinary HTTP)
HTTP handlers use Header<T> for auth and return HttpResult<Json<T>>:
#![allow(unused)]
fn main() {
use afast::{get, post, put, delete, Tag};
use serde::{Deserialize, Serialize};
// HTTP auth uses Header, not Custom
#[derive(Debug, Deserialize, Tag)]
#[tag("HTTP auth header")]
pub struct AuthHeader {
#[tag("Authorization header")]
pub authorization: String,
}
impl AuthHeader {
pub fn token(&self) -> &str {
self.authorization.strip_prefix("Bearer ").unwrap_or(&self.authorization)
}
}
#[derive(Debug, Deserialize, Tag)]
#[tag("Query parameters")]
pub struct ListUsersQuery {
#[tag("Page number")]
pub page: Option<i64>,
#[tag("Items per page")]
pub size: Option<i64>,
}
#[derive(Debug, Serialize, Tag)]
#[tag("User HTTP response")]
pub struct UserHttp {
#[tag("User ID")]
pub id: i64,
#[tag("Username")]
pub username: String,
}
#[derive(Debug, Serialize, Tag)]
#[tag("User list response")]
pub struct ListUsersHttpResponse {
#[tag("Total count")]
pub total: i64,
#[tag("User list")]
pub items: Vec<UserHttp>,
}
// Note: #[get] contains desc, NOT a path! Path is defined in service! macro
#[get(desc("List users via HTTP"))]
pub async fn list_users_http(
afast::State(state): afast::State<AppState>,
afast::Header(auth): afast::Header<AuthHeader>,
afast::Query(query): afast::Query<ListUsersQuery>,
) -> afast::HttpResult<afast::Json<ListUsersHttpResponse>> {
let db = state.db.lock().await;
let _user_id = db.get_user_id_by_token(auth.token()).await
.ok_or_else(|| afast::Error::custom(401, "invalid token"))?;
let items = db.read(0, 100).await;
Ok(afast::Json(ListUsersHttpResponse { total: items.len() as i64, items: vec![] }))
}
}
Service Registration
Paths are defined in the service! macro, NOT in handler attributes:
use afast::{AFast, service};
// Binary handlers are wrapped in h()
// HTTP handler paths are defined in service!
let admin_svc = service!("admin", "Admin Service" => {
group("user" => {
// Binary protocol handlers
h(create_user),
h(list_users),
// HTTP REST handlers β paths defined here
get("", list_users_http),
post("", create_user_http),
group(":user_id" => {
get("", get_user_http),
put("", update_user_http),
delete("", delete_user_http),
})
})
});
// Catch-all route
let check_svc = service!("check", "Check Service" => {
h(health),
get("*", catch_all_get),
});
#[tokio::main]
async fn main() {
AFast::new()
.state(AppState::new())
.service(admin_svc)
.service(check_svc)
.http("0.0.0.0:5000")
.run()
.await
.unwrap();
}
AppState Pattern
#![allow(unused)]
fn main() {
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone)]
pub struct AppState {
pub db: Arc<Mutex<Database>>,
}
impl AppState {
pub fn new() -> Self {
Self { db: Arc::new(Mutex::new(Database::new())) }
}
}
}
Type Derivation Rules
Binary protocol types β for Data<T> and return values in #[handler]:
#![allow(unused)]
fn main() {
#[derive(AFastDeserialize, Tag)]
#[tag("Request description")]
pub struct MyRequest {
#[tag("Field description")]
pub field: String,
}
#[derive(AFastSerialize, Tag)]
#[tag("Response description")]
pub struct MyResponse {
#[tag("Field description")]
pub field: String,
}
}
HTTP types β for Body<T>, Query<T> and return values in #[get]/#[post] (requires serde):
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Tag)]
#[tag("HTTP request body")]
pub struct MyBody {
#[tag("Field description")]
pub field: String,
}
#[derive(Debug, Serialize, Tag)]
#[tag("HTTP response")]
pub struct MyHttpResponse {
#[tag("Field description")]
pub field: String,
}
}
Enum types β generates tagged union in TypeScript:
#![allow(unused)]
fn main() {
#[derive(AFastSerialize, AFastDeserialize, Tag)]
#[tag("User role")]
pub enum Role {
#[tag("Administrator")]
Admin,
#[tag("Regular user")]
User,
#[tag("Guest")]
Guest,
}
// Generated TS: type Role = { tag: 'Admin', data: null } | { tag: 'User', data: null } | ...
}
Error Handling
Use afast::Error::custom(code, message) to return custom errors:
#![allow(unused)]
fn main() {
#[handler(desc("Handler with error handling"))]
pub async fn with_error(
afast::State(state): afast::State<AppState>,
) -> afast::Result<MyResponse> {
if some_condition {
return Err(afast::Error::custom(400, "Bad request"));
}
let data = state.db.lock().await.query().await
.map_err(|e| afast::Error::custom(500, e.to_string()))?;
Ok(MyResponse { message: "Success".into() })
}
}
HTTP Methods (Ordinary Routes)
Key differences between HTTP and binary handlers:
- Auth uses
Header<T>instead ofCustom<T> - Returns
afast::HttpResult<afast::Json<T>>instead ofafast::Result<T> - Types need
serde::Deserialize/serde::Serialize+Tag - Paths are defined in
service!macro, NOT in#[get]attributes
#![allow(unused)]
fn main() {
use afast::{get, post, put, delete, Tag};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Tag)]
#[tag("Query parameters")]
pub struct ListUsersQuery {
#[tag("Page number")]
pub page: Option<i64>,
#[tag("Items per page")]
pub size: Option<i64>,
}
#[derive(Debug, Deserialize, Tag)]
#[tag("Create user request")]
pub struct CreateUserBody {
#[tag("Username")]
pub username: String,
#[tag("Password")]
pub password: String,
#[tag("Display name")]
pub name: String,
}
#[derive(Debug, Deserialize, Tag)]
#[tag("User ID path parameter")]
pub struct UserIdParam {
#[tag("User ID")]
pub user_id: i64,
}
// Paths are defined in service! macro, only desc here
#[get(desc("List users"))]
pub async fn list_users_http(
afast::State(state): afast::State<AppState>,
afast::Header(auth): afast::Header<AuthHeader>,
afast::Query(query): afast::Query<ListUsersQuery>,
) -> afast::HttpResult<afast::Json<ListUsersHttpResponse>> {
// GET /user?page=1&size=10
Ok(afast::Json(ListUsersHttpResponse { total: 0, items: vec![] }))
}
#[post(desc("Create user"))]
pub async fn create_user_http(
afast::State(state): afast::State<AppState>,
afast::Header(auth): afast::Header<AuthHeader>,
afast::Body(body): afast::Body<CreateUserBody>,
) -> afast::HttpResult<afast::Json<CreateUserHttpResponse>> {
// POST /user with JSON body
Ok(afast::Json(CreateUserHttpResponse { id: 1 }))
}
// Registration in service!:
// service!("admin", "Admin Service" => {
// group("user" => {
// get("", list_users_http), // GET /user
// post("", create_user_http), // POST /user
// group(":user_id" => {
// get("", get_user_http), // GET /user/:user_id
// put("", update_user_http), // PUT /user/:user_id
// delete("", delete_user_http), // DELETE /user/:user_id
// })
// })
// })
}
WebSocket Handlers
Path is defined in service! macro:
#![allow(unused)]
fn main() {
use afast::ws;
use afast::extractors::{WsSender, WsReceiver};
#[ws(desc("Chat WebSocket"))]
pub async fn chat_ws(
afast::State(state): afast::State<AppState>,
sender: WsSender,
receiver: WsReceiver,
) {
while let Some(msg) = receiver.recv().await {
sender.send(msg).await;
}
}
// Registration in service!:
// let chat_svc = service!("chat", "Chat Service" => {
// ws("/chat/:room", chat_ws),
// });
}
SSE (Server-Sent Events)
Path is defined in service! macro:
#![allow(unused)]
fn main() {
use afast::sse;
use afast::extractors::SseSender;
#[sse(desc("Event stream"))]
pub async fn sse_stream(sender: SseSender) {
for i in 0..10 {
sender.send(format!("Event {}", i)).await;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
// Registration in service!:
// let chat_svc = service!("chat", "Chat Service" => {
// sse("/sse", sse_stream),
// });
}
Lifecycle Hooks
#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext, RequestGuard, ConnectionGuard};
struct MyHook;
impl Hook for MyHook {
fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
eprintln!("β {} ({})", ctx.handler_name, ctx.transport);
Some(Box::new(MyTimer(std::time::Instant::now())))
}
fn on_connect(&self, ctx: &RequestContext) -> Option<Box<dyn ConnectionGuard>> {
eprintln!("β connect: {}", ctx.handler_name);
Some(Box::new(MyConnGuard))
}
}
struct MyTimer(std::time::Instant);
struct MyConnGuard;
impl RequestGuard for MyTimer {
fn on_response(&mut self, ctx: &RequestContext, _resp: &[u8]) {
eprintln!("β {} OK ({:?})", ctx.handler_name, self.0.elapsed());
}
fn on_error(&mut self, ctx: &RequestContext, err: &afast::Error) {
eprintln!("β {} error: {}", ctx.handler_name, err);
}
}
impl ConnectionGuard for MyConnGuard {
fn on_disconnect(&mut self, ctx: &RequestContext) {
eprintln!("β disconnect: {}", ctx.handler_name);
}
}
// Register in main:
// AFast::new().hook(MyHook)
}
Rate Limiting
Use rate_limit("policy_name") in #[handler] attribute, and configure the policy in main:
#![allow(unused)]
fn main() {
use afast::RateLimitConfig;
#[handler(desc("Rate limited endpoint"), rate_limit("api_limit"))]
pub async fn limited_endpoint(
afast::State(state): afast::State<AppState>,
) -> afast::Result<MyResponse> {
Ok(MyResponse { message: "Success".into() })
}
// Configure rate limiting in main:
// AFast::new()
// .rate_limit(RateLimitConfig::new()
// .policy("api_limit", afast::RateLimitPolicy::fixed_window(100, 60)))
}
Client Code Generation
After starting the server, generate client code per service:
# TypeScript
curl http://localhost:5000/code/auth/ts
# JavaScript
curl http://localhost:5000/code/auth/js
# Kotlin
curl http://localhost:5000/code/auth/kt
# Rust
curl http://localhost:5000/code/auth/rs
Where auth is the service name. Each service generates a separate client file.
Client Usage Examples
TypeScript / JavaScript Client
The generated TS client creates a Client class per service, exposing all handler methods via the apis property.
import { AuthClient } from './auth';
import { AdminClient } from './admin';
// ββ Binary protocol client (fetch mode, most common) ββ
const auth = new AuthClient({
host: 'localhost',
port: 5001,
tls: false,
transport: 'fetch', // 'fetch' | 'ws' | 'nodetcp' | 'buntcp'
debug: true, // prints request/response logs
customs: {
// Custom<T> extractor requires a Promise-returning factory
AuthCustom: async () => ({ token: myToken })
}
});
await auth.apis._ready; // wait for connection (fetch is instant)
// Call binary handler β fully typed params and return values
const reg = await auth.apis.signup({
username: 'alice',
password: 'secret',
name: 'Alice'
});
console.log(reg.user.username); // 'alice'
console.log(reg.token);
const login = await auth.apis.login({
username: 'alice',
password: 'secret'
});
// Handler with no params
const uid = await auth.apis.get_user_id();
// ββ Service with both binary and HTTP routes ββ
const admin = new AdminClient({
host: 'localhost',
port: 5001,
tls: false,
transport: 'fetch',
customs: {
AuthCustom: async () => ({ token: myToken })
},
headers: {
// Header<T> for HTTP route auth
AuthHeader: async () => ({ authorization: `Bearer ${myToken}` })
}
});
await admin.apis._ready;
// Binary handler β accessed via nested group
const users = await admin.apis.user.list_users({ page: 1, size: 10 });
console.log(users.total, users.items);
const newUser = await admin.apis.user.create_user({
username: 'bob', password: 'pw', name: 'Bob'
});
// HTTP REST handler β accessed via nested group
const httpUsers = await admin.apis.user.list_users_http({
queries: { page: 1, size: 10 } // Query params go in queries
});
const created = await admin.apis.user.create_user_http({
body: { username: 'charlie', password: 'pw', name: 'Charlie' } // Body goes in body
});
// Path params are extracted from the param object
const updated = await admin.apis.user.user_id.update_user_http({
user_id: 123, // path param :user_id
body: { name: 'Updated', age: 25, active: true }
});
// ββ WebSocket mode (supports long connections and push) ββ
const wsAuth = new AuthClient({
host: 'localhost',
port: 3001,
tls: false,
transport: 'ws',
customs: { AuthCustom: async () => ({ token: myToken }) }
});
await wsAuth.apis._ready;
// Same API, binary frames over WebSocket
const result = await wsAuth.apis.login({ username: 'alice', password: 'secret' });
Kotlin Client
import afast.generated.*
fun main() = runBlocking {
val auth = AuthClient(
host = "localhost",
port = 5001,
tls = false,
transport = "http", // "http" | "ws" | "tcp"
customFns = AuthCustomFns(
AuthCustom = { AuthCustom(token = myToken) }
)
)
// suspend functions, call directly in coroutine
val reg = auth.apis.signup(RegisterRequest(
username = "alice",
password = "secret",
name = "Alice"
))
println(reg.user.username) // "alice"
println(reg.token)
val login = auth.apis.login(LoginRequest(
username = "alice",
password = "secret"
))
val uid = auth.apis.get_user_id()
// Service with HTTP routes
val admin = AdminClient(
host = "localhost",
port = 5001,
tls = false,
transport = "http",
customFns = AdminCustomFns(
AuthCustom = { AuthCustom(token = myToken) }
),
headerFns = AdminHeaderFns(
AuthHeader = { AuthHeader(authorization = "Bearer $myToken") }
)
)
// Binary handler
val users = admin.apis.user.list_users(ListUsersRequest(page = 1, size = 10))
// HTTP REST handler
val httpUsers = admin.apis.user.list_users_http(
queries = UserListUsersHttpQuery(page = 1, size = 10)
)
}
Client Type Generation Rules
| Rust Type | TypeScript Type | Notes |
|---|---|---|
String | string | |
i32/i64/u32/u64/f32/f64 | number | |
bool | boolean | |
Vec<T> | T[] | |
Option<T> | T | null | |
HashMap<K,V> | Record<K,V> | |
Vec<u8> | Uint8Array | |
enum { A, B } | { tag: 'A', data: null } | { tag: 'B', data: null } | Tagged union |
#[handler(name("signup"))] | apis.signup(...) | Client method name |
group("user" => { ... }) | apis.user.xxx(...) | Nested group |
Supported Client Transports
| Transport | Description | Use Case |
|---|---|---|
fetch | HTTP/1.1 or HTTP/2 | Browser, Node.js (most common) |
ws | WebSocket binary frames | Browser, Node.js long connections |
nodetcp | Node.js TCP | Node.js high-performance |
buntcp | Bun TCP | Bun runtime |
unirequest | uni-app HTTP | Mini programs / mobile apps |
uniws | uni-app WebSocket | Mini programs long connections |
wxrequest | WeChat Mini Program HTTP | WeChat mini programs |
wxws | WeChat Mini Program WebSocket | WeChat mini program long connections |
Common Mistakes to Avoid
- Donβt forget
Tagderive β All types used in handlers must deriveTagwith#[tag("description")]on fields - Donβt use
Stringfor request body β Always useData<T>with proper struct - Donβt forget
Resultreturn type β Binary handlers returnafast::Result<T>, HTTP handlers returnafast::HttpResult<afast::Json<T>> - Donβt manually register routes β Use
#[handler]macro - Donβt clone State β
State<T>holds&'static T, just use it directly - Parameters must use destructuring β Write
afast::State(state): afast::State<AppState>notstate: State<AppState> - HTTP auth uses Header β HTTP handlers use
Header<T>for auth, notCustom<T> - HTTP types need serde β HTTP handler request/response types need
Deserialize/Serialize+Tag #[get]takes desc not path β Paths are defined inservice!macro- Path params use
:paramin service! β e.g.group(":user_id" => { get("", handler) })
Project Structure Template
my-project/
βββ Cargo.toml
βββ src/
βββ main.rs # Entry point, service! definitions and AFast config
βββ state.rs # AppState + Database definitions
βββ handler/
βββ mod.rs
βββ auth.rs # Authentication handlers
βββ admin.rs # Admin handlers (including HTTP routes)
βββ chat.rs # WebSocket/SSE handlers
Quick Reference
| Macro | Purpose | Example |
|---|---|---|
#[handler] | Binary protocol handler | #[handler(desc("..."), name("..."), cache(60), rate_limit("..."))] |
#[get] | HTTP GET | #[get(desc("..."))] β path in service! |
#[post] | HTTP POST | #[post(desc("..."))] |
#[put] | HTTP PUT | #[put(desc("..."))] |
#[delete] | HTTP DELETE | #[delete(desc("..."))] |
#[ws] | WebSocket | #[ws(desc("..."))] |
#[sse] | SSE | #[sse(desc("..."))] |
service! | Create service with routes | service!("name", "desc" => { h(fn), get("path", fn) }) |
h() | Register binary handler | h(my_handler) |
group() | Route grouping | group("user" => { get("", fn), group(":id" => { get("", fn) }) }) |
| Extractor | Purpose | Destructuring Syntax |
|---|---|---|
State<T> | Shared app state | afast::State(state): afast::State<AppState> |
Data<T> | Binary request body | afast::Data(req): afast::Data<MyRequest> |
Custom<T> | Binary auth context | afast::Custom(auth): afast::Custom<AuthCustom> |
Ctx<T> | Request context (hook-set) | afast::Ctx(ctx): afast::Ctx<RequestInfo> |
Query<T> | HTTP query parameters | afast::Query(q): afast::Query<MyQuery> |
Param<T> | HTTP path parameters | afast::Param(p): afast::Param<MyParam> |
Body<T> | HTTP request body | afast::Body(b): afast::Body<MyBody> |
Header<T> | HTTP request headers | afast::Header(h): afast::Header<AuthHeader> |