AFast
็ฎไฝไธญๆ | English
AFast is a high-performance Rust web backend framework. It eliminates manual route
definitions โ annotate functions with #[handler] and the framework auto-registers
and dispatches requests. Data transport uses a compact binary protocol that is
smaller and faster than JSON. It supports one-click generation of TypeScript,
JavaScript, Kotlin, and Rust client code, with built-in interactive API documentation.
Highlights
- Zero Route Definitions โ
#[handler]annotation, no manual routing table - Compact Binary Protocol โ Smaller and faster than JSON, designed for internal communication
- Auto Code Generation โ TypeScript / JavaScript / Kotlin / Rust clients with full type definitions
- Interactive API Docs โ Built-in Web docs with dark/light theme and online API testing
- Multiple Transports โ WebSocket, HTTP/1.1, HTTP/2, and TCP, mix and match as needed
- TLS / HTTPS โ Based on rustls with ALPN for HTTP/2 negotiation
- Lifecycle Hooks โ
before_request/on_response/on_error/on_connect/on_disconnect - Rate Limiting โ Named-policy rate limiting with pluggable storage backend
Quick Example
use afast::{AFast, handler, service, State, Data, Result};
use afast::{AFastDeserialize, AFastSerialize, Tag};
#[derive(Clone)]
struct AppState { db_url: String }
#[derive(AFastDeserialize, Tag)]
#[tag("Request body")]
struct HelloReq { name: String }
#[derive(AFastSerialize, Tag)]
#[tag("Response body")]
struct HelloResp { message: String }
#[handler(desc("Say hello"))]
async fn hello(
state: State<AppState>,
req: Data<HelloReq>,
) -> Result<HelloResp> {
Ok(HelloResp { message: format!("Hello, {}!", req.name) })
}
#[tokio::main]
async fn main() {
let svc = service!("api" => { h(hello) });
AFast::new()
.state(AppState { db_url: "localhost".into() })
.service(svc)
.http("0.0.0.0:5000")
.run().await.unwrap();
}
Quick Start
Add Dependency
[dependencies]
afast = { version = "0.1.9", features = ["http", "ws", "ts"] }
tokio = { version = "1", features = ["full"] }
Define a Handler
use afast::{AFast, handler, service, State, Data, Custom, Result};
use afast::{AFastDeserialize, AFastSerialize, Tag};
#[derive(Clone)]
struct AppState { db_url: String }
#[derive(Clone)]
struct CacheState { redis_url: String }
#[derive(AFastDeserialize, Tag)]
#[tag("Auth info")]
struct AuthCustom { token: i64, platform: String }
#[derive(AFastDeserialize, Tag)]
#[tag("Request body")]
struct HelloReq { name: String }
#[derive(AFastSerialize, Tag)]
#[tag("Response body")]
struct HelloResp { message: String }
#[handler(desc("Say hello"), name("hello"))]
async fn hello(
state: State<AppState>,
cache: State<CacheState>,
auth: Custom<AuthCustom>,
req: Data<HelloReq>,
) -> Result<HelloResp> {
println!("DB: {}, Cache: {}", state.db_url, cache.redis_url);
Ok(HelloResp { message: format!("Hello, {}!", req.name) })
}
#[tokio::main]
async fn main() {
let svc = service!("api", "Example API" => {
h(hello),
});
let app = AFast::new()
.state(AppState { db_url: "localhost".into() })
.state(CacheState { redis_url: "redis://localhost".into() })
.service(svc)
.ws("0.0.0.0:3000")
.http("0.0.0.0:5000");
app.run().await.unwrap();
}
Run
cargo run --features "http,ws,ts"
- WebSocket API:
ws://localhost:3000 - HTTP API:
POST http://localhost:5000/_api - Generated TS client code in
./code/api.ts
Features
| Feature | Description | Dependencies |
|---|---|---|
http | HTTP server | hyper, hyper-util, http-body-util |
ws | WebSocket server | tokio-tungstenite, futures-util |
tcp | TCP server (length-prefix framing) | โ |
ts | TypeScript client generation (ESM + full types) | โ |
js | JavaScript client generation (ESM + JSDoc) | โ |
kt | Kotlin client generation | โ |
rs | Rust client generation (Tokio async / std sync TCP) | โ |
code | On-demand code generation at /code/{service}/{lang} | http |
doc | Interactive API docs at /doc endpoint | http, js |
ordinary-http | RESTful JSON endpoints (GET/POST/PUT/DELETE) | http, serde, serde_json |
seq64 | WS request ID uses i64 (default i32) | โ |
len64 | WS payload length uses u64 (default u32) | โ |
tag-u8 | Enum tag uses u8 (default) | afastdata/tag-u8 |
tag-u16 | Enum tag uses u16 | afastdata/tag-u16 |
tag-u32 | Enum tag uses u32 | afastdata/tag-u32 |
marker | Enable marker-based conditional serialization; set marker via AFast::marker() (default "afast") | โ |
hook | Lifecycle hooks (before_request/on_response/on_error/on_connect/on_disconnect), global and per-service | โ |
rate-limit | Named-policy rate limiting (FixedWindow/SlidingWindow/TokenBucket), pluggable store (built-in InMemoryStore) | โ |
Note: If the server uses
seq64orlen64, generated client code must use the same feature, otherwise protocol mismatch will occur.
Core Concepts
Handler Registration
The #[handler] proc macro generates the following at compile time:
- The original function unchanged
HandlerMetaโ name, description, parameter list, return type metadataHandlerInvokertrait impl โ type-erased invoker, deserializes params, calls the function- 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 commentsname("...")โ 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
| Extractor | Description | Protocols |
|---|---|---|
State<T> | Injects shared state by type from StateMap (T: Clone) | All |
Data<T> | Deserializes request body from binary payload | HTTP/WS/TCP |
Custom<T> | Deserializes client-side custom context (e.g., auth token) | HTTP/WS/TCP |
Receiver | Receives binary messages from the client (long connection) | WS/TCP |
Sender | Sends 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
| Rule | Example | Description |
|---|---|---|
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 |
Conditional Serialization (Marker)
When the marker feature is enabled, AFast::marker() sets a global marker string (default "afast") passed to afastdataโs to_bytes_with / from_bytes_with. Fields annotated with #[afast(skip_with("marker"))] are conditionally skipped during serialization/deserialization based on the active marker.
Skip Modes
#[afast(skip)]โ Field is always skipped, never serialized/deserialized. Must have aDefaultimpl or initialization function.#[afast(skip_with("marker"))]โ Skipped when the marker matches; serialized normally otherwise.
The marker propagates recursively into nested types (Vec<T>, Option<T>, etc.).
Generated client code (TS/JS/KT/RS) and API docs automatically exclude skipped fields.
Example
#![allow(unused)]
fn main() {
#[derive(AFastSerialize, AFastDeserialize, Tag)]
#[tag("User info")]
struct User {
name: String,
#[afast(skip)]
internal_secret: String, // always skipped
#[afast(skip_with("afast"))]
internal_note: String, // skipped when marker is "afast"
}
let app = AFast::new()
.marker("afast") // set marker; default is already "afast"
.service(svc)
.http("0.0.0.0:5000");
}
Without the marker feature, serialize / deserialize use plain to_bytes / from_bytes and all fields are always included. However, #[afast(skip)] fields are still excluded from generated client code.
Rate Limiting
Enable the rate-limit feature to apply named rate-limit policies to handlers. Supports HTTP, WebSocket, and TCP transports.
Configuration
#![allow(unused)]
fn main() {
use afast::{RateLimitConfig, RateLimitPolicy, RateLimitKey, Algorithm};
let app = AFast::new()
.rate_limit(
RateLimitConfig::new()
.policy(RateLimitPolicy {
id: "login".into(),
max_requests: 5,
window_secs: 60,
key: RateLimitKey::Ip,
algorithm: Algorithm::SlidingWindow,
})
.default_policy("global")
.policy(RateLimitPolicy {
id: "global".into(),
max_requests: 100,
window_secs: 1,
key: RateLimitKey::Ip,
algorithm: Algorithm::SlidingWindow,
}),
)
.service(svc)
.http("0.0.0.0:5000");
}
Binding a Handler
#![allow(unused)]
fn main() {
#[handler(rate_limit("login"), desc("User login"))]
async fn login(
state: State<AppState>,
req: Data<LoginRequest>,
) -> Result<LoginResponse> {
// ...
}
}
Handlers without rate_limit automatically use the default_policy. If no default is set, they are not rate-limited.
Rate Limit Keys
| Key | Description | HTTP | WebSocket | TCP |
|---|---|---|---|---|
Ip | Client IP (supports X-Forwarded-For) | โ | โ | โ |
Header("name") | HTTP header value (e.g. API Key) | โ | โ (cached at handshake) | โญ skipped |
Connection | Per-connection (WS/TCP message rate) | โญ skipped | โ | โ |
Global | Shared global counter | โ | โ | โ |
Storage Backend
The default InMemoryStore keeps counters in process memory. Implement RateLimitStore for a custom backend (e.g. Redis):
#![allow(unused)]
fn main() {
use afast::RateLimitStore;
struct RedisStore { /* ... */ }
impl RateLimitStore for RedisStore {
fn incr<'a>(&'a self, key: &'a str, ttl_secs: u64)
-> Pin<Box<dyn Future<Output = u64> + Send + 'a>> { /* INCR + EXPIRE */ }
fn get<'a>(&'a self, key: &'a str)
-> Pin<Box<dyn Future<Output = u64> + Send + 'a>> { /* GET */ }
fn set<'a>(&'a self, key: &'a str, value: u64, ttl_secs: u64)
-> Pin<Box<dyn Future<Output = ()> + Send + 'a>> { /* SET + EXPIRE */ }
fn delete<'a>(&'a self, key: &'a str)
-> Pin<Box<dyn Future<Output = ()> + Send + 'a>> { /* DEL */ }
}
}
Rejection Response
- HTTP: Status
429 Too Many Requests, body:{"code":-90012,"message":"Too many requests"} - WebSocket / TCP: Error frame with code
-90012
Customize via RateLimitConfig::rejected_code() and rejected_message().
Lifecycle Hooks
Enable the hook feature to intercept request lifecycle events for observability, tracing, logging, or custom middleware.
Example
#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext, RequestGuard};
struct TimingHook;
impl Hook for TimingHook {
fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
println!("โ {} ({})", ctx.handler_name, ctx.transport);
Some(Box::new(std::time::Instant::now()))
}
}
impl RequestGuard for std::time::Instant {
fn on_response(&mut self, ctx: &RequestContext, _resp: &[u8]) {
println!("โ {} OK ({:?})", ctx.handler_name, self.elapsed());
}
fn on_error(&mut self, ctx: &RequestContext, err: &afast::Error) {
println!("โ {} error: {}", ctx.handler_name, err);
}
}
}
Global and Service Hooks
#![allow(unused)]
fn main() {
let app = AFast::new()
.hook(TimingHook) // Global: all handlers
.service(
service!("api" => { h(handler) })
.hook(ApiSpecificHook) // Service: only this service's handlers
);
}
- Global hooks run for every handler.
- Service hooks run only for handlers in that service.
- Both always execute โ they never replace each other.
- Execution order: global first, then service (onion model ๐ง
).
before_request: Hook1 โ Hook2 โ handleron_response/on_error: Guard2 โ Guard1 (reversed)
Long Connection Hooks
For WebSocket/TCP long-connection handlers, the full lifecycle is:
on_connect โ before_request โ handler โ on_response/on_error โ on_disconnect
RequestContext Fields
| Field | Type | Description |
|---|---|---|
handler_name | &'static str | Handler function name |
handler_desc | &'static str | Description from #[handler(desc(...))] |
transport | &'static str | "tcp", "ws", or "http" |
handler_id | usize | Handler offset in dispatch table |
state | Arc<StateMap> | Shared application state |
Transport Layer
AFast supports multiple transport layers that can run simultaneously on different ports.
HTTP
HTTP server endpoints:
| Method | Path | Description |
|---|---|---|
| POST | /_api | Binary handler dispatch |
| GET | /_ws | WebSocket upgrade (merged mode) |
| GET | /code/{service}/{lang} | On-demand code gen (requires code) |
| GET | /doc | API docs index (requires doc) |
| GET | /doc/{service} | Service-specific docs (requires doc) |
| * | Ordinary routes | RESTful endpoints (requires ordinary-http) |
HTTP Response Format:
- Success:
[0u8][0i64][data: bytes] - Error:
[1u8][code: i64][message: bytes]
WebSocket
WS frame format:
Request: [req_id: SeqId][handler_id: u32][len: Len][payload]
Push: [0: SeqId][conn_id: u32][len: Len][payload]
Heartbeat:[0xFFFFFFFF: SeqId][len: Len][conn_id1: u32]...
SeqId type is controlled by the seq64 feature (i32 or i64), Len type by the len64 feature (u32 or u64).
TCP
TCP uses 4-byte big-endian length-prefix framing with complete binary payloads per frame. Suitable for embedded devices or raw TCP scenarios.
HTTP + WS Port Merging
When ws_addr and http_addr are set to the same address, AFast merges WebSocket into the HTTP server via HTTP Upgrade:
#![allow(unused)]
fn main() {
// Same port for both HTTP and WebSocket
let app = AFast::new()
.service(svc)
.ws("0.0.0.0:5000")
.http("0.0.0.0:5000"); // Same address, auto-merged
}
Ordinary HTTP (REST)
With ordinary-http, define RESTful routes using get/post/put/patch/delete inside the service! macro:
#![allow(unused)]
fn main() {
use afast::{get, Query, Param, Body, Header, Json, HttpResult};
#[derive(Deserialize)]
struct UserQuery { page: i64, size: i64 }
#[derive(Serialize)]
struct UserResponse { id: i64, name: String }
#[get(":id")]
async fn get_user(
state: State<AppState>,
param: Param<UserPath>,
query: Query<UserQuery>,
) -> HttpResult<Json<UserResponse>> {
Ok(Json(UserResponse { id: param.id, name: format!("User {}", param.id) }))
}
}
Response Types
| Type | HTTP Status | Content-Type |
|---|---|---|
Json<T> | 200 | application/json |
Text | 200 | text/plain |
Html | 200 | text/html |
File | 200 | Custom + Content-Disposition: attachment |
Status(code) | Custom | โ |
Redirect(url) | 302 | Location header |
Long Connections
Handlers using Receiver/Sender are auto-detected as long-connection mode:
#![allow(unused)]
fn main() {
#[handler(desc("Chat"))]
async fn chat(
state: State<AppState>,
auth: Custom<Auth>,
mut receiver: Receiver,
sender: Sender,
) -> Result<()> {
sender.send(b"Welcome!".to_vec()).await?;
while let Some(msg) = receiver.recv().await {
sender.send(msg).await?; // echo
}
Ok(())
}
}
Generated clients return a Socket object for long-connection handlers, with send()/close() and onMessage callback.
Code Generation
Static Generation (compile-time file output)
#![allow(unused)]
fn main() {
use afast::{GenerateTarget, Lang, JsTsCallType, RsCallType};
let app = AFast::new()
.service(api_svc)
.generate(vec![
GenerateTarget {
lang: Lang::TS(vec![JsTsCallType::Fetch, JsTsCallType::Ws]),
path: "./code".into(),
debug: false,
},
GenerateTarget {
lang: Lang::RS(vec![RsCallType::TcpAsync]),
path: "./src/bin/client".into(),
debug: true,
},
]);
}
Dynamic Generation (HTTP endpoint)
GET /code/api/ts?call=fetch,ws
GET /code/api/js?call=fetch,ws
GET /code/pay/kt?call=http,ws,tcp
GET /code/api/rs?call=tcp-async
Supported Transport Types
TS/JS
| Value | API |
|---|---|
fetch | Browser fetch |
ws | Browser WebSocket |
nodetcp | Node.js net |
buntcp | Bun Bun.connect |
unirequest | UniApp uni.request |
uniws | UniApp uni.connectSocket |
wxrequest | WeChat Mini Program wx.request |
wxws | WeChat Mini Program wx.connectSocket |
Kotlin
| Value | API |
|---|---|
http / fetch | java.net.HttpURLConnection |
ws | java.net.http.WebSocket |
tcp | java.net.Socket |
Rust
| Value | API |
|---|---|
tcp-async | tokio::net::TcpStream (async) |
tcp-sync | std::net::TcpStream (sync) |
Client Usage
import { ApiClient } from './api';
// Dedicated WS port
const wsClient = new ApiClient('ws://localhost:3000');
const wsResult = await wsClient.apis.user.list_users({ page: 1, size: 20 });
// Merged mode (WS and HTTP on the same port)
const mergedClient = new ApiClient('ws://localhost:5000');
// Auto-connects to ws://localhost:5000/_ws
The client transport mode is fixed at construction time.
Client-Side Caching
The cache(seconds) attribute enables client-side caching:
#![allow(unused)]
fn main() {
#[handler(desc("List users"), cache(60))]
async fn list_users(...) -> Result<ListUsersResponse> { /* ... */ }
}
Generated client:
const users = await client.apis.admin.listUsers({ page: 1, size: 20 });
// Within 60 seconds, same params return cached data
const fresh = await client.apis.admin.listUsers({ page: 1, size: 20 }, true);
// force = true bypasses cache
About TextEncoder / TextDecoder
The generated client code uses TextEncoder and TextDecoder APIs. These are unavailable on React Native (older versions), WeChat Mini Programs, and older browsers.
Solutions:
- Polyfill:
npm install text-encoding+import 'text-encoding' - React Native 0.72+: Built-in support
- WeChat/UniApp: Use
wxrequest/wxws/unirequest/uniwstransport types
Binary Protocol
Type Mapping
| Rust Type | TS/JS Type | Kotlin Type |
|---|---|---|
i8 ~ i64, u8 ~ u64, f32, f64 | number | Int/Long/Float/Double |
bool | boolean | Boolean |
String, &str | string | String |
Vec<u8> | Uint8Array | ByteArray |
Option<T> | T | null | T? |
Vec<T> | T[] | List<T> |
| struct | { field: Type } | data class |
| enum | { tag: 'Variant', data: ... } | sealed class |
Error Codes
System reserved error codes range from -90011 to -90000. User-defined errors must not use this range.
| Constant | Value | Description |
|---|---|---|
CODE_SIGNAL | -90000 | OS signal (e.g., Ctrl+C) |
CODE_MSG_TOO_SHORT | -90001 | Message too short |
CODE_PAYLOAD_MISMATCH | -90002 | Payload length mismatch |
CODE_SERIALIZE | -90003 | Serialization/deserialization error |
CODE_STATE_NOT_FOUND | -90004 | State type not registered |
CODE_HANDLER | -90005 | Handler execution error |
CODE_INVALID_PARAM | -90006 | Invalid parameter |
CODE_IO | -90007 | I/O error |
CODE_WS | -90008 | WebSocket error |
CODE_HTTP | -90009 | HTTP error |
CODE_TCP | -90010 | TCP error |
CODE_LONG_CONNECTION_NOT_SUPPORTED | -90011 | Long connections unsupported in HTTP mode |
CODE_RATE_LIMITED | -90012 | Rate limit exceeded |
#![allow(unused)]
fn main() {
// Custom error (code must be outside the reserved range)
return Err(afast::Error::custom(400, "invalid request parameter"));
}
Interactive Documentation
With the doc feature, visit http://host:port/doc for interactive API docs:
#![allow(unused)]
fn main() {
let app = AFast::new()
.service(svc)
.document(afast::DocConfig::new()
.title("My API Documentation")
.output("./docs") // Also write static HTML to disk
)
.http("0.0.0.0:5000");
}
GET /docโ Index page listing all servicesGET /doc/{service}โ Service docs with type definitions and online test panel- Dark/light theme toggle
- Online test panel can send real requests to the server
Project Structure
afast/ โ Main framework crate (core types, State, transports, code generation)
afast-macros/ โ Proc macros (#[handler], register!, #[derive(Tag)])
example/ โ Example project (full usage including HTTP, WS, TCP, docs)
Dependencies
afastโafast-macros,afastdata,tokioafast-macrosโsyn,quote,proc-macro2- User crates indirectly depend on
afastdata-core(referenced by#[derive(Tag)]expanded code)
Testing
# Start the example server
cargo run -p example
# Run the test client
cargo run -p example --bin test_client
License
MIT