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

RustAPI Cookbook

Welcome to the RustAPI Architecture Cookbook. This documentation is designed to be the single source of truth for the project’s philosophy, patterns, and practical implementation details.

Note

This is a living document. As our architecture evolves, so will this cookbook.

What is this?

This is not just API documentation. This is a collection of:

  • Keynotes: High-level architectural decisions and “why” we made them.
  • Patterns: The repeated structures (like Action and Service) that form the backbone of our code.
  • Recipes: Practical, step-by-step guides for adding features, testing, and maintaining cleanliness.

Visual Identity

This cookbook is styled with the RustAPI Premium Dark theme, focusing on readability, contrast, and modern “glassmorphism” aesthetics.

Quick Start

Getting Started

Welcome to RustAPI. This section will guide you from installation to your first running API.

Installation

Note

RustAPI is designed for Rust 1.75 or later.

Prerequisites

Before we begin, ensure you have the Rust toolchain installed. If you haven’t, the best way is via rustup.rs.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Installing the CLI

RustAPI comes with a powerful CLI to scaffold projects. Install it directly from crates.io:

cargo install cargo-rustapi

Verify your installation:

cargo rustapi --version

Editor Setup

For the best experience, we recommend VS Code with the rust-analyzer extension. This provides:

  • Real-time error checking
  • Intelligent code completion
  • In-editor documentation

Quickstart

Tip

From zero to a production-ready API in 60 seconds.

Create a New Project

Use the CLI to generate a new project. We’ll call it my-api.

cargo rustapi new my-api
cd my-api

This commands sets up a complete project structure with handling, models, and tests ready to go.

Run the Server

Start your API server:

cargo run

You should see output similar to:

INFO 🚀 Server running at http://127.0.0.1:8080
INFO 📚 API docs at http://127.0.0.1:8080/docs

Test It Out

Open your browser to http://127.0.0.1:8080/docs.

You’ll see the Swagger UI automatically generated from your code. Try out the /health endpoint or create a new Item in the Items API.

What Just Happened?

You just launched a high-performance, async Rust web server with:

  • ✅ Automatic OpenAPI documentation
  • ✅ Type-safe request validation
  • ✅ Distributed tracing
  • ✅ Global error handling

Welcome to RustAPI.

Project Structure

RustAPI projects follow a standard, modular structure designed for scalability.

my-api/
├── Cargo.toml          // Dependencies and workspace config
├── src/
│   ├── handlers/       // Request handlers (Controllers)
│   │   ├── mod.rs      
│   │   └── items.rs    // Example resource handler
│   ├── models/         // Data structures and Schema
│   │   ├── mod.rs      
│   ├── error.rs        // Custom error types
│   └── main.rs         // Application entry point & Router
└── .env.example        // Environment variables template

Key Files

src/main.rs

The heart of your application. This is where you configure the RustApi builder, register routes, and set up state.

src/handlers/

Where your business logic lives. Handlers are async functions that take extractors (like Json, Path, State) and return responses.

src/models/

Your data types. By deriving Schema, they automatically appear in your OpenAPI documentation.

src/error.rs

Centralized error handling. Mapping your AppError to ApiError allows you to simply return Result<T, AppError> in your handlers.

Core Concepts

Documentation of the fundamental architectural decisions and patterns in RustAPI.

Handlers & Extractors

The Handler is the fundamental unit of work in RustAPI. It transforms an incoming HTTP request into an outgoing HTTP response.

Unlike many web frameworks that enforce a strict method signature (e.g., fn(req: Request, res: Response)), RustAPI embraces a flexible, type-safe approach powered by Rust’s trait system.

The Philosophy: “Ask for what you need”

In RustAPI, you don’t manually parse the request object inside your business logic. Instead, you declare the data you need as function arguments, and the framework’s Extractors handle the plumbing for you.

If the data cannot be extracted (e.g., missing header, invalid JSON), the request is rejected before your handler is ever called. This means your handler logic is guaranteed to operate on valid, type-safe data.

Anatomy of a Handler

A handler is simply an asynchronous function that takes zero or more Extractors as arguments and returns something that implements IntoResponse.

#![allow(unused)]
fn main() {
use rustapi::prelude::*;

async fn create_user(
    State(db): State<DbPool>,         // 1. Dependency Injection
    Path(user_id): Path<Uuid>,        // 2. URL Path Parameter
    Json(payload): Json<CreateUser>,  // 3. JSON Request Body
) -> Result<impl IntoResponse, ApiError> {
    
    let user = db.create_user(user_id, payload).await?;
    
    Ok((StatusCode::CREATED, Json(user)))
}
}

Key Rules

  1. Order Matters (Slightly): Extractors that consume the request body (like Json<T> or Multipart) must be the last argument. This is because the request body is a stream that can only be read once.
  2. Async by Default: Handlers are async fn. This allows non-blocking I/O operations (DB calls, external API requests).
  3. Debuggable: Handlers are just functions. You can unit test them easily.

Extractors: The FromRequest Trait

Extractors are types that implement FromRequest (or FromRequestParts for headers/query params). They isolate the “HTTP parsing” logic from your “Business” logic.

Common Build-in Extractors

ExtractorSourceExample Usage
Path<T>URL Path Segmentsfn get_user(Path(id): Path<u32>)
Query<T>Query Stringfn search(Query(params): Query<SearchFn>)
Json<T>Request Bodyfn update(Json(data): Json<UpdateDto>)
HeaderMapHTTP Headersfn headers(headers: HeaderMap)
State<T>Application Statefn db_op(State(pool): State<PgPool>)
Extension<T>Request-local extensionsfn logic(Extension(user): Extension<User>)

Custom Extractors

You can create your own extractors to encapsulate repetitive validation or parsing logic. For example, extracting a user ID from a verified JWT:

#![allow(unused)]
fn main() {
pub struct AuthenticatedUser(pub Uuid);

#[async_trait]
impl<S> FromRequestParts<S> for AuthenticatedUser
where
    S: Send + Sync,
{
    type Rejection = ApiError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let auth_header = parts.headers.get("Authorization")
            .ok_or(ApiError::Unauthorized("Missing token"))?;
        
        let token = auth_header.to_str().map_err(|_| ApiError::Unauthorized("Invalid token"))?;
        let user_id = verify_jwt(token)?; // Your verification logic
        
        Ok(AuthenticatedUser(user_id))
    }
}

// Usage in handler: cleaner and reusable!
async fn profile(AuthenticatedUser(uid): AuthenticatedUser) -> impl IntoResponse {
    format!("User ID: {}", uid)
}
}

Responses: The IntoResponse Trait

A handler can return any type that implements IntoResponse. RustAPI provides implementations for many common types:

  • StatusCode (e.g., return 200 OK or 404 Not Found)
  • Json<T> (serializes struct to JSON)
  • String / &str (plain text response)
  • Vec<u8> / Bytes (binary data)
  • HeaderMap (response headers)
  • Html<String> (HTML content)

Tuple Responses

You can combine types using tuples to set status codes and headers along with the body:

#![allow(unused)]
fn main() {
// Returns 201 Created + JSON Body
async fn create() -> (StatusCode, Json<User>) {
    (StatusCode::CREATED, Json(user))
}

// Returns Custom Header + Plain Text
async fn custom() -> (HeaderMap, &'static str) {
    let mut headers = HeaderMap::new();
    headers.insert("X-Custom", "Value".parse().unwrap());
    (headers, "Response with headers")
}
}

Error Handling

Handlers often return Result<T, E>. If the handler returns Ok(T), the T is converted to a response. If it returns Err(E), the E is converted to a response.

This effectively means your Error type must implement IntoResponse.

#![allow(unused)]
fn main() {
// Recommended pattern: Centralized API Error enum
pub enum ApiError {
    NotFound(String),
    InternalServerError,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            ApiError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong".to_string()),
        };
        
        (status, Json(json!({ "error": message }))).into_response()
    }
}
}

Best Practices

  1. Keep Handlers Thin: Move complex business logic to “Service” structs or domain modules. Handlers should focus on HTTP translation (decoding request -> calling service -> encoding response).
  2. Use State for Dependencies: Avoid global variables. Pass DB pools and config via State.
  3. Parse Early: Use specific types in Json<T> structs rather than serde_json::Value to leverage the type system for validation.

Performance Philosophy

RustAPI is built on a simple premise: Abstractions shouldn’t cost you runtime performance.

We leverage Rust’s unique ownership system and modern async ecosystem (Tokio, Hyper) to deliver performance that rivals C++ servers, while maintaining developer safe-guards.

The Pillars of Speed

1. Zero-Copy Networking

Where possible, RustAPI avoids copying memory. When you receive a large JSON payload or file upload, we aim to pass pointers to the underlying memory buffer rather than cloning the data.

  • Bytes over Vec<u8>: We use the bytes crate extensively. Passing a Bytes object around is O(1) (it’s just a reference-counted pointer and length), whereas cloning a Vec<u8> is O(n).
  • String View: Extractors like Path and Query often leverage Cow<'str, str> (Clone on Write) to avoid allocations if the data doesn’t need to be modified.

2. Multi-Core Async Runtime

RustAPI runs on Tokio, a work-stealing, multi-threaded runtime.

  • Non-blocking I/O: A single thread can handle thousands of concurrent idle connections (e.g., WebSockets waiting for messages) with minimal memory overhead.
  • Work Stealing: If one CPU core is overloaded with tasks, other idle cores will “steal” work from its queue, ensuring balanced utilization of your hardware.

3. Compile-Time Router

Our router (matchit) is based on a Radix Trie structure.

  • O(log n) Lookup: Route matching speed depends on the length of the URL, not the number of routes defined. Having 10 routes or 10,000 routes has negligible impact on routing latency.
  • Allocation-Free Matching: For standard paths, routing decisions happen without heap allocations.

Memory Management

Stack vs. Heap

RustAPI encourages stack allocation for small, short-lived data.

  • Extractors are often allocated on the stack.
  • Response bodies are streamed, meaning a 1GB file download doesn’t require 1GB of RAM. It flows through a small, constant-sized buffer.

Connection Pooling

For database performance, we strongly recommend using connection pooling (e.g., sqlx::Pool).

  • Reuse: Establishing a TCP connection and performing a simplified SSL handshake for every request is slow. Pooling keeps connections open and ready.
  • Multiplexing: Some drivers allow multiple queries to be in-flight on a single connection simultaneously.

Optimizing Your App

To get the most out of RustAPI, follow these guidelines:

  1. Avoid Blocking the Async Executor: Never run CPU-intensive tasks (cryptography, image processing) or blocking I/O (std::fs::read) directly in an async handler.

    • Solution: Use tokio::task::spawn_blocking to offload these to a dedicated thread pool.
    #![allow(unused)]
    fn main() {
    // BAD: Blocks the thread, potentially stalling other requests
    fn handler() {
        let digest = tough_crypto_hash(data); 
    }
    
    // GOOD: Runs on a thread meant for blocking work
    async fn handler() {
        let digest = tokio::task::spawn_blocking(move || {
            tough_crypto_hash(data)
        }).await.unwrap();
    }
    }
  2. JSON Serialization: While serde is fast, JSON text processing is CPU heavy.

    • For extremely high-throughput endpoints, consider binary formats like Protobuf or MessagePack if the client supports it.
  3. Keep State Light: Your State struct is cloned for every request. Wrap large shared data in Arc<T> so only the pointer is cloned, not the data itself.

#![allow(unused)]
fn main() {
// Fast
#[derive(Clone)]
struct AppState {
    db: PgPool,                // Internally uses Arc
    config: Arc<Config>,       // Wrapped in Arc manually
}
}

Benchmarking

Performance is not a guessing game. Use tools like wrk, k6, or drill to stress-test your specific endpoints.

# Example using drill
drill --benchmark benchmark.yml --stats

Remember: RustAPI provides the capability for high performance, but your application logic ultimately dictates the speed. Use the tools wisely.

Testing Strategy

Reliable software requires a robust testing strategy. RustAPI is designed to be testable at every level, from individual functions to full end-to-end scenarios.

The Testing Pyramid

We recommend a balanced approach:

  1. Unit Tests (70%): Fast, isolated tests for individual logic pieces.
  2. Integration Tests (20%): Testing handlers and extractors wired together.
  3. End-to-End (E2E) Tests (10%): Testing the running server from the outside.

1. Unit Testing Handlers

Since handlers are just regular functions, you can unit test them by invoking them directly. However, dealing with Extractors directly in tests can sometimes be verbose.

Often, it is better to extract your “Business Logic” into a separate function or trait, test that thoroughly, and keep the Handler layer thin.

#![allow(unused)]
fn main() {
// Domain Logic (Easy to test)
fn calculate_total(items: &[Item]) -> u32 {
    items.iter().map(|i| i.price).sum()
}

// Handler (Just plumbing)
async fn checkout(Json(cart): Json<Cart>) -> Json<Receipt> {
    let total = calculate_total(&cart.items);
    Json(Receipt { total })
}
}

2. Integration Testing with Tower

RustAPI routers implement tower::Service. This means you can send requests to your router directly in memory without spawning a TCP server or using localhost. This is extremely fast.

We rely on tower::util::ServiceExt to call the router.

Setup

Add tower and http-body-util for testing utilities:

[dev-dependencies]
tower = { version = "0.4", features = ["util"] }
http-body-util = "0.1"
tokio = { version = "1", features = ["full"] }

Example Test

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_create_user() {
    // 1. Build the app (same as in main.rs)
    let app = app(); 

    // 2. Construct a Request
    let response = app
        .oneshot(
            Request::builder()
                .method(http::Method::POST)
                .uri("/users")
                .header(http::header::CONTENT_TYPE, "application/json")
                .body(Body::from(r#"{"username": "alice"}"#))
                .unwrap(),
        )
        .await
        .unwrap();

    // 3. Assert Status
    assert_eq!(response.status(), StatusCode::CREATED);

    // 4. Assert Body
    let body_bytes = response.into_body().collect().await.unwrap().to_bytes();
    let body: User = serde_json::from_slice(&body_bytes).unwrap();
    assert_eq!(body.username, "alice");
}
}

3. Mocking Dependencies with State

To test handlers that rely on databases or external APIs, you should mock those dependencies.

Use Traits to define the capabilities, and use generics or dynamic dispatch in your State.

#![allow(unused)]
fn main() {
// 1. Define the interface
#[async_trait]
trait UserRepository: Send + Sync {
    async fn get_user(&self, id: u32) -> Option<User>;
}

// 2. Real Implementation
struct PostgresRepo { pool: PgPool }

// 3. Mock Implementation
struct MockRepo;
#[async_trait]
impl UserRepository for MockRepo {
    async fn get_user(&self, _id: u32) -> Option<User> {
        Some(User { username: "mock_user".into() })
    }
}

// 4. Use in Handler
async fn get_user(
    State(repo): State<Arc<dyn UserRepository>>, // Accepts any impl
    Path(id): Path<u32>
) -> Json<User> {
    // ...
}
}

In your tests, inject Arc::new(MockRepo) into the State.

4. End-to-End Testing

For E2E tests, you can spawn the actual server on a random port and use a real HTTP client (like reqwest) to hit it.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn e2e_test() {
    // Binding to port 0 lets the OS choose a random available port
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();

    // Spawn server in background
    tokio::spawn(async move {
        RustApi::serve(listener, app()).await.unwrap();
    });

    // Make real requests
    let client = reqwest::Client::new();
    let resp = client.get(format!("http://{}/health", addr))
        .send()
        .await
        .unwrap();

    assert!(resp.status().is_success());
}
}

This approach is slower but validates strictly everything, including network serialization and actual TCP behavior.

Crate Deep Dives

Warning

This section is for those who want to understand the framework’s internal organs. You don’t need to know this to use RustAPI, but it helps if you want to master it.

RustAPI is a collection of focused, interoperable crates. Each crate has a specific philosophy and “Lens” through which it views the world.

rustapi-core: The Engine

rustapi-core is the foundational crate of the framework. It provides the essential types and traits that glue everything together, although application developers typically interact with the facade crate rustapi.

Core Responsibilities

  1. Routing: Mapping HTTP requests to Handlers.
  2. Extraction: The FromRequest trait definition.
  3. Response: The IntoResponse trait definition.
  4. Middleware: The Layer and Service integration with Tower.

The Router Internals

We use matchit, a high-performance Radix Tree implementation for routing.

Why Radix Trees?

  • Speed: Lookup time is proportional to the length of the path, not the number of routes.
  • Priority: Specific paths (/users/profile) always take precedence over wildcards (/users/:id), regardless of definition order.
  • Parameters: Efficiently parses named parameters like :id or *path without regular expressions.

The Handler Trait Magic

The Handler trait is what allows you to write functions with arbitrary arguments.

#![allow(unused)]
fn main() {
// This looks simple...
async fn my_handler(state: State<Db>, json: Json<Data>) { ... }

// ...but under the hood, it compiles to something like:
impl Handler for my_handler {
    fn call(req: Request) -> Future<Output=Response> {
        // 1. Extract State
        // 2. Extract Json
        // 3. Call original function
        // 4. Convert return to Response
    }
}
}

This is achieved through recursive trait implementations on tuples. RustAPI supports handlers with up to 16 arguments.

Middleware Architecture

rustapi-core is built on top of tower. This means any standard Tower middleware works out of the box.

#![allow(unused)]
fn main() {
// The Service stack looks like an onion:
// Outer Layer (Timeout)
//  -> Middle Layer (Trace)
//      -> Inner Layer (Router)
//          -> Handler
}

When you call .layer(), you are wrapping the inner service with a new outer layer.

The BoxRoute

To keep compilation times fast and types manageable, the Router eventually “erases” the specific types of your handlers into a BoxRoute (a boxed tower::Service). This is a dynamic dispatch boundary that trades a tiny amount of runtime performance (nanoseconds) for significantly faster compile times and usability.

rustapi-macros: The Magic

rustapi-macros reduces boilerplate by generating code at compile time.

#[debug_handler]

The most important macro for beginners. Rust’s error messages for complex generic traits (like Handler) can be notoriously difficult to understand.

If your handler doesn’t implement the Handler trait (e.g., because you used an argument that isn’t a valid Extractor), the compiler might give you an error spanning the entire RustApi::new() chain, miles away from the actual problem.

#[debug_handler] fixes this.

It verifies the handler function in isolation and produces clear error messages pointing exactly to the invalid argument.

#![allow(unused)]
fn main() {
#[debug_handler]
async fn handler(
    // Compile Error: "String" does not implement FromRequest. 
    // Did you mean "Json<String>" or "Body"?
    body: String 
) { ... }
}

#[derive(FromRequest)]

Automatically implement FromRequest for your structs.

#![allow(unused)]
fn main() {
#[derive(FromRequest)]
struct MyExtractor {
    // These fields must themselves be Extractors
    header: HeaderMap,
    body: Json<MyData>,
}

// Now you can use it in a handler
async fn handler(input: MyExtractor) {
    println!("{:?}", input.header);
}
}

This is heavily used to group multiple extractors into a single struct (often called the “Parameter Object” pattern), keeping function signatures clean.

rustapi-validate: The Gatekeeper

Data validation should happen at the edges of your system, before invalid data ever reaches your business logic. rustapi-validate integrates the validator crate directly into RustAPI’s extraction flow.

The Validate Trait

First, define your rules using attributes on your struct.

#![allow(unused)]
fn main() {
use rustapi_validate::Validate;
use serde::Deserialize;

#[derive(Debug, Deserialize, Validate)]
pub struct SignupRequest {
    #[validate(length(min = 3, message = "Username too short"))]
    pub username: String,

    #[validate(email(message = "Invalid email format"))]
    pub email: String,

    #[validate(range(min = 18, max = 150))]
    pub age: u8,
}
}

The ValidatedJson Extractor

Instead of using the standard Json<T>, use ValidatedJson<T>.

#![allow(unused)]
fn main() {
use rustapi_validate::ValidatedJson;

async fn signup(
    ValidatedJson(payload): ValidatedJson<SignupRequest>
) -> impl IntoResponse {
    // If we reach here, 'payload' is guaranteed to be valid!
    // No need to check if email includes '@' or age >= 18.
    
    process_signup(payload)
}
}

Automatic Error Handling

If validation fails, ValidatedJson automatically returns a 400 Bad Request response with a structured JSON error body detailing exactly which fields failed and why.

{
  "error": "Validation Failed",
  "fields": {
    "email": ["Invalid email format"],
    "age": ["Must be at least 18"]
  }
}

Custom Validation logic

You can also write custom validation functions.

#![allow(unused)]
fn main() {
#[derive(Validate)]
struct Request {
    #[validate(custom = "validate_premium_status")]
    code: String,
}

fn validate_premium_status(code: &str) -> Result<(), rustapi_validate::ValidationError> {
    if !code.starts_with("PREMIUM_") {
        return Err(rustapi_validate::ValidationError::new("Invalid premium code"));
    }
    Ok(())
}
}

rustapi-openapi: The Cartographer

Lens: “The Cartographer” Philosophy: “Documentation as Code.”

Automatic Spec Generation

We believe that if documentation is manual, it is wrong. RustAPI uses utoipa to generate an OpenAPI 3.0 specification directly from your code.

The Schema Trait

Any type that is part of your API (request or response) must implement Schema.

#![allow(unused)]
fn main() {
#[derive(Schema)]
struct Metric {
    /// The name of the metric
    name: String,
    
    /// Value (0-100)
    #[schema(minimum = 0, maximum = 100)]
    value: i32,
}
}

Operation Metadata

Use macros to enrich endpoints:

#![allow(unused)]
fn main() {
#[rustapi::get("/metrics")]
#[rustapi::tag("Metrics")]
#[rustapi::summary("List all metrics")]
#[rustapi::response(200, Json<Vec<Metric>>)]
async fn list_metrics() -> Json<Vec<Metric>> { ... }
}

Swagger UI

The RustApi builder automatically mounts a Swagger UI at the path you specify:

#![allow(unused)]
fn main() {
RustApi::new()
    .docs("/docs") // Mounts Swagger UI at /docs
    // ...
}

rustapi-extras: The Toolbox

Lens: “The Toolbox” Philosophy: “Batteries included, but swappable.”

Feature Flags

This crate is a collection of production-ready middleware. Everything is behind a feature flag so you don’t pay for what you don’t use.

FeatureComponent
jwtJwtLayer, AuthUser extractor
corsCorsLayer
auditAuditStore, AuditLogger
rate-limitRateLimitLayer

Middleware Usage

Middleware wraps your entire API or specific routes.

#![allow(unused)]
fn main() {
let app = RustApi::new()
    .layer(CorsLayer::permissive())
    .layer(CompressionLayer::new())
    .route("/", get(handler));
}

Audit Logging

For enterprise compliance (GDPR/SOC2), the audit feature provides a structured way to record sensitive actions.

#![allow(unused)]
fn main() {
async fn delete_user(
    AuthUser(user): AuthUser,
    State(audit): State<AuditLogger>
) {
    audit.log(AuditEvent::new("user.deleted")
        .actor(user.id)
        .target("user_123")
    );
}
}

rustapi-toon: The Diplomat

Lens: “The Diplomat” Philosophy: “Optimizing for Silicon Intelligence.”

What is TOON?

Token-Oriented Object Notation is a format designed to be consumed by Large Language Models (LLMs). It reduces token usage by stripping unnecessary syntax (braces, quotes) while maintaining semantic structure.

Content Negotiation

The LlmResponse<T> type automatically negotiates the response format based on the Accept header.

#![allow(unused)]
fn main() {
async fn agent_data() -> LlmResponse<Data> {
    // Returns JSON for browsers
    // Returns TOON for AI Agents (using fewer tokens)
}
}

Token Savings

TOON often reduces token count by 30-50% compared to JSON, saving significant costs and context window space when communicating with models like GPT-4 or Gemini.

rustapi-ws: The Live Wire

Lens: “The Live Wire” Philosophy: “Real-time, persistent connections made simple.”

The WebSocket Extractor

Upgrading an HTTP connection to a WebSocket uses the standard extractor pattern:

#![allow(unused)]
fn main() {
async fn ws_handler(
    ws: WebSocket,
) -> impl IntoResponse {
    ws.on_upgrade(handle_socket)
}
}

Architecture

We recommend an Actor Model for WebSocket state.

  1. Each connection spawns a new async task (the actor).
  2. Use tokio::sync::broadcast channels for global events (like chat rooms).
  3. Use mpsc channels for direct messaging.

rustapi-view: The Artist

Lens: “The Artist” Philosophy: “Server-side rendering with modern tools.”

Tera Integration

We use Tera, a Jinja2-like template engine, for rendering HTML on the server.

#![allow(unused)]
fn main() {
async fn home(
    State(templates): State<Templates>
) -> View {
    let mut ctx = Context::new();
    ctx.insert("user", "Alice");
    
    View::new("home.html", ctx)
}
}

Layouts and Inheritance

Tera supports template inheritance, allowing you to define a base layout (base.html) and extend it in child templates (index.html), keeping your frontend DRY.

rustapi-jobs: The Workhorse

Lens: “The Workhorse” Philosophy: “Fire and forget, with reliability guarantees.”

Background Processing

Long-running tasks shouldn’t block HTTP requests. rustapi-jobs provides a robust queue system.

#![allow(unused)]
fn main() {
// Define a job
#[derive(Serialize, Deserialize)]
struct EmailJob { to: String }

// Enqueue it
queue.push(EmailJob { to: "alice@example.com" }).await;
}

Backends

  • Memory: Great for development and testing.
  • Redis: High throughput persistence.
  • Postgres: Transactional reliability (acid).

Reliability

The worker system features:

  • Exponential Backoff: Automatic retries for failing jobs.
  • Dead Letter Queue: Poison jobs are isolated for manual inspection.

rustapi-testing: The Auditor

Lens: “The Auditor” Philosophy: “Trust, but verify.”

The TestClient

Integration testing is often painful. We make it easy. TestClient spawns your RustApi application without binding to a real TCP port, communicating directly with the service layer.

#![allow(unused)]
fn main() {
let client = TestClient::new(app);
}

Fluent Assertions

The client provides a fluent API for making requests and asserting responses.

#![allow(unused)]
fn main() {
client.post("/login")
    .json(&credentials)
    .send()
    .await
    .assert_status(200)
    .assert_header("Set-Cookie", "session=...");
}

Mocking Services

Because rustapi-rs relies heavily on Dependency Injection via State<T>, you can easily inject mock implementations of your database or downstream services when creating the RustApi instance for your test.

cargo-rustapi: The Architect

Lens: “The Architect” Philosophy: “Scaffolding best practices from day one.”

The CLI

The RustAPI CLI isn’t just a project generator; it’s a productivity multiplier.

Commands

  • cargo rustapi new <name>: Create a new project with the perfect directory structure.
  • cargo rustapi generate resource <name>: Scaffold a new API resource (Model + Handlers + Tests).
  • cargo rustapi serve: Run the development server with hot reload (future feature).

Templates

The templates used by the CLI are opinionated but flexible. They enforce:

  • Modular folder structure.
  • Implementation of State pattern.
  • Separation of Error types.

Recipes

Recipes are practical, focused guides to solving specific problems with RustAPI.

Format

Each recipe follows a simple structure:

  1. Problem: What are we trying to solve?
  2. Solution: The code.
  3. Discussion: Why it works and what to watch out for.

Table of Contents

Creating Resources

Problem: You need to add a new “Resource” (like Users, Products, or Posts) to your API with standard CRUD operations.

Solution

Create a new module src/handlers/users.rs:

#![allow(unused)]
fn main() {
use rustapi_rs::prelude::*;

#[derive(Serialize, Deserialize, Schema, Clone)]
pub struct User {
    pub id: u64,
    pub name: String,
}

#[derive(Deserialize, Schema)]
pub struct CreateUser {
    pub name: String,
}

#[rustapi::get("/users")]
pub async fn list() -> Json<Vec<User>> {
    Json(vec![]) // Fetch from DB in real app
}

#[rustapi::post("/users")]
pub async fn create(Json(payload): Json<CreateUser>) -> impl IntoResponse {
    let user = User { id: 1, name: payload.name };
    (StatusCode::CREATED, Json(user))
}
}

Then register it in main.rs:

#![allow(unused)]
fn main() {
RustApi::new()
    .mount(handlers::users::list)
    .mount(handlers::users::create)
}

Discussion

Using #[rustapi::mount] (if available) or manual routing keeps your main.rs clean. Organizing handlers by resource (domain-driven design) scales better than organizing by HTTP method.

JWT Authentication

Authentication is critical for almost every API. This recipe demonstrates how to implement JSON Web Token (JWT) authentication using the jsonwebtoken crate and RustAPI’s extractor pattern.

Dependencies

Add jsonwebtoken and serde to your Cargo.toml:

[dependencies]
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }

1. Define Claims

The standard JWT claims. You can add custom fields here (like role).

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,  // Subject (User ID)
    pub exp: usize,   // Expiration time
    pub role: String, // Custom claim: "admin", "user"
}
}

2. Configuration State

Store your keys in the application state.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use jsonwebtoken::{EncodingKey, DecodingKey};

#[derive(Clone)]
pub struct AuthState {
    pub encoder: EncodingKey,
    pub decoder: DecodingKey,
}

impl AuthState {
    pub fn new(secret: &str) -> Self {
        Self {
            encoder: EncodingKey::from_secret(secret.as_bytes()),
            decoder: DecodingKey::from_secret(secret.as_bytes()),
        }
    }
}
}

3. The AuthUser Extractor

This is where the magic happens. We create a custom extractor that:

  1. Checks the Authorization header.
  2. Decodes the token.
  3. Validates expiration.
  4. Returns the claims or rejects the request.
#![allow(unused)]
fn main() {
use rustapi::prelude::*;
use jsonwebtoken::{decode, Validation, Algorithm};

pub struct AuthUser(pub Claims);

#[async_trait]
impl FromRequestParts<Arc<AuthState>> for AuthUser {
    type Rejection = (StatusCode, Json<serde_json::Value>);

    async fn from_request_parts(
        parts: &mut Parts, 
        state: &Arc<AuthState>
    ) -> Result<Self, Self::Rejection> {
        // 1. Get header
        let auth_header = parts.headers.get("Authorization")
            .ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Missing token"}))))?;
            
        let token = auth_header.to_str()
            .map_err(|_| (StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token format"}))))?
            .strip_prefix("Bearer ")
            .ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token type"}))))?;

        // 2. Decode
        let token_data = decode::<Claims>(
            token, 
            &state.decoder, 
            &Validation::new(Algorithm::HS256)
        ).map_err(|e| (StatusCode::UNAUTHORIZED, Json(json!({"error": e.to_string()}))))?;

        Ok(AuthUser(token_data.claims))
    }
}
}

4. Usage in Handlers

Now, securing an endpoint is as simple as adding an argument.

#![allow(unused)]
fn main() {
async fn protected_profile(
    AuthUser(claims): AuthUser
) -> Json<String> {
    Json(format!("Welcome back, {}! You are a {}.", claims.sub, claims.role))
}

async fn login(State(state): State<Arc<AuthState>>) -> Json<String> {
    // In a real app, validate credentials first!
    let claims = Claims {
        sub: "user_123".to_owned(),
        role: "admin".to_owned(),
        exp: 10000000000, // Future timestamp
    };

    let token = jsonwebtoken::encode(
        &jsonwebtoken::Header::default(), 
        &claims, 
        &state.encoder
    ).unwrap();

    Json(token)
}
}

5. Wiring it Up

#[tokio::main]
async fn main() {
    let auth_state = Arc::new(AuthState::new("my_secret_key"));

    let app = RustApi::new()
        .route("/login", post(login))
        .route("/profile", get(protected_profile))
        .with_state(auth_state); // Inject state

    RustApi::serve("127.0.0.1:3000", app).await.unwrap();
}

Bonus: Role-Based Access Control (RBAC)

Since we have the role in our claims, we can enforce permissions easily.

#![allow(unused)]
fn main() {
async fn admin_only(AuthUser(claims): AuthUser) -> Result<String, StatusCode> {
    if claims.role != "admin" {
        return Err(StatusCode::FORBIDDEN);
    }
    Ok("Sensitive Admin Data".to_string())
}
}

Database Integration

RustAPI is database-agnostic, but SQLx is the recommended driver due to its async-first design and compile-time query verification.

This recipe shows how to integrate PostgreSQL/MySQL/SQLite using a global connection pool.

Dependencies

[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"

1. Setup Connection Pool

Create the pool once at startup and share it via State.

use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;

pub struct AppState {
    pub db: sqlx::PgPool,
}

#[tokio::main]
async fn main() {
    dotenvy::dotenv().ok();
    let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    // Create a connection pool
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&db_url)
        .await
        .expect("Failed to connect to DB");

    // Run migrations (optional but recommended)
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await
        .expect("Failed to migrate");

    let state = Arc::new(AppState { db: pool });

    let app = RustApi::new()
        .route("/users", post(create_user))
        .with_state(state);

    RustApi::serve("0.0.0.0:3000", app).await.unwrap();
}

2. Using the Database in Handlers

Extract the State to get access to the pool.

#![allow(unused)]
fn main() {
use rustapi::prelude::*;

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email: String,
}

#[derive(Serialize)]
struct User {
    id: i32,
    username: String,
    email: String,
}

async fn create_user(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), ApiError> {
    
    // SQLx query macro performs compile-time checking!
    let record = sqlx::query_as!(
        User,
        "INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id, username, email",
        payload.username,
        payload.email
    )
    .fetch_one(&state.db)
    .await
    .map_err(|e| ApiError::InternalServerError(e.to_string()))?;

    Ok((StatusCode::CREATED, Json(record)))
}
}

3. Dependency Injection for Testing

To make testing easier, define a trait for your database operations. This allows you to swap the real DB for a mock.

#![allow(unused)]
fn main() {
#[async_trait]
pub trait UserRepository: Send + Sync {
    async fn create(&self, username: &str, email: &str) -> anyhow::Result<User>;
}

// Production implementation
pub struct PostgresRepo(sqlx::PgPool);

#[async_trait]
impl UserRepository for PostgresRepo {
    async fn create(&self, username: &str, email: &str) -> anyhow::Result<User> {
        // ... impl ...
    }
}
}

Then update your state to hold the trait object:

#![allow(unused)]
fn main() {
struct AppState {
    // Dyn dispatch allows swapping impls at runtime
    db: Arc<dyn UserRepository>,
}
}

Error Handling

Don’t expose raw SQL errors to users. Map them to your ApiError type.

#![allow(unused)]
fn main() {
impl From<sqlx::Error> for ApiError {
    fn from(err: sqlx::Error) -> Self {
        match err {
            sqlx::Error::RowNotFound => ApiError::NotFound("Resource not found".into()),
            _ => {
                // Log the real error internally
                tracing::error!("Database error: {:?}", err);
                // Return generic error to user
                ApiError::InternalServerError
            }
        }
    }
}
}

File Uploads

Handling file uploads efficiently is crucial. RustAPI allows you to stream Multipart data, meaning you can handle 1GB uploads without using 1GB of RAM.

Dependencies

[dependencies]
rustapi = { version = "0.1", features = ["multipart"] }
tokio = { version = "1", features = ["fs", "io-util"] }
uuid = { version = "1", features = ["v4"] }

Streaming Upload Handler

This handler reads the incoming stream part-by-part and writes it directly to disk (or S3).

#![allow(unused)]
fn main() {
use rustapi::prelude::*;
use rustapi::extract::Multipart;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

async fn upload_file(mut multipart: Multipart) -> Result<StatusCode, ApiError> {
    // Iterate over the fields
    while let Some(field) = multipart.next_field().await.map_err(|_| ApiError::BadRequest)? {
        
        let name = field.name().unwrap_or("file").to_string();
        let file_name = field.file_name().unwrap_or("unknown.bin").to_string();
        let content_type = field.content_type().unwrap_or("application/octet-stream").to_string();

        println!("Uploading: {} ({})", file_name, content_type);

        // Security: Create a safe random filename to prevent overwrites or path traversal
        let new_filename = format!("{}-{}", uuid::Uuid::new_v4(), file_name);
        let path = std::path::Path::new("./uploads").join(new_filename);

        // Open destination file
        let mut file = File::create(&path).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?;

        // Write stream to file chunk by chunk
        let mut field_bytes = field; // field implements Stream itself (in some drivers) or we read chunks
        
        // In RustAPI/Axum multipart, `field.bytes()` loads the whole field into memory.
        // To stream, we use `field.chunk()`:
        
        while let Some(chunk) = field.chunk().await.map_err(|_| ApiError::BadRequest)? {
             file.write_all(&chunk).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?;
        }
    }

    Ok(StatusCode::CREATED)
}
}

Handling Constraints

You should always set limits to prevent DoS attacks.

#![allow(unused)]
fn main() {
use rustapi::extract::DefaultBodyLimit;

let app = RustApi::new()
    .route("/upload", post(upload_file))
    // Limit request body to 10MB
    .layer(DefaultBodyLimit::max(10 * 1024 * 1024));
}

Validating Content Type

Never trust the Content-Type header sent by the client implicitly for security (e.g., executing a PHP script uploaded as an image).

Verify the “magic bytes” of the file content itself if strictly needed, or ensure uploaded files are stored in a non-executable directory (or S3 bucket).

#![allow(unused)]
fn main() {
// Simple check on the header (not fully secure but good UX)
if let Some(ct) = field.content_type() {
    if !ct.starts_with("image/") {
        return Err(ApiError::BadRequest("Only images are allowed".into()));
    }
}
}

Custom Middleware

Problem: You need to execute code before or after every request (e.g., logging, metrics).

Solution

Implement a tower::Layer.

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct MyMiddleware<S> { inner: S }

impl<S, B> Service<Request<B>> for MyMiddleware<S>
where S: Service<Request<B>> {
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request<B>) -> Self::Future {
        println!("Request: {}", req.uri());
        self.inner.call(req)
    }
}
}

Discussion

For simple cases, you can use tower_http::TraceLayer or middleware::from_fn instead of writing a full struct.

Real-time Chat (WebSockets)

WebSockets allow full-duplex communication between the client and server. RustAPI leverages the rustapi-ws crate (based on tungstenite and tokio) to make this easy.

Dependencies

[dependencies]
rustapi-ws = "0.1"
tokio = { version = "1", features = ["sync"] }
futures = "0.3"

The Upgrade Handler

WebSocket connections start as HTTP requests. We “upgrade” them.

#![allow(unused)]
fn main() {
use rustapi_ws::{WebSocket, WebSocketUpgrade, Message};
use rustapi::prelude::*;
use std::sync::Arc;
use tokio::sync::broadcast;

// Shared state for broadcasting messages to all connected clients
pub struct AppState {
    pub tx: broadcast::Sender<String>,
}

async fn ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
    // Finalize the upgrade and spawn the socket handler
    ws.on_upgrade(|socket| handle_socket(socket, state))
}
}

Handling the Connection

#![allow(unused)]
fn main() {
use futures::{sink::SinkExt, stream::StreamExt};

async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
    // Split the socket into a sender and receiver
    let (mut sender, mut receiver) = socket.split();

    // Subscribe to the global broadcast channel
    let mut rx = state.tx.subscribe();

    // Spawn a task to forward broadcast messages to this client
    let mut send_task = tokio::spawn(async move {
        while let Ok(msg) = rx.recv().await {
            // If the client disconnects, this will fail and we break
            if sender.send(Message::Text(msg)).await.is_err() {
                break;
            }
        }
    });

    // Handle incoming messages from THIS client
    let mut recv_task = tokio::spawn(async move {
        while let Some(Ok(msg)) = receiver.next().await {
            if let Message::Text(text) = msg {
                println!("Received message: {}", text);
                // Broadcast it to everyone else
                let _ = state.tx.send(format!("User says: {}", text));
            }
        }
    });

    // Wait for either task to finish (disconnection)
    tokio::select! {
        _ = (&mut send_task) => recv_task.abort(),
        _ = (&mut recv_task) => send_task.abort(),
    };
}
}

Initialization

#[tokio::main]
async fn main() {
    // Create a broadcast channel with capacity of 100 messages
    let (tx, _rx) = broadcast::channel(100);
    let state = Arc::new(AppState { tx });

    let app = RustApi::new()
        .route("/ws", get(ws_handler))
        .with_state(state);

    RustApi::serve("0.0.0.0:3000", app).await.unwrap();
}

Client-Side Testing

You can simpler use JavaScript in the browser console:

let ws = new WebSocket("ws://localhost:3000/ws");

ws.onmessage = (event) => {
    console.log("Message from server:", event.data);
};

ws.send("Hello form JS!");

Advanced Patterns

  1. User Authentication: Use the same AuthUser extractor in the ws_handler. If authentication fails, return an error before upgrading.
  2. Ping/Pong: Browsers and Load Balancers kill idle connections. Implement a heartbeat mechanism to keep the connection alive.
    • rustapi-ws handles low-level ping/pong frames automatically in many cases, but application-level pings are also robust.

Production Tuning

Problem: Your API needs to handle extreme load (10k+ requests per second).

Solution

1. Release Profile

Ensure Cargo.toml has optimal settings:

[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true

2. Runtime Config

Configure the Tokio runtime for high throughput in main.rs:

#[tokio::main(worker_threads = num_cpus::get())]
async fn main() {
    // ...
}

3. File Descriptors (Linux)

Increase the limit before running:

ulimit -n 100000

Discussion

RustAPI is fast by default, but the OS often becomes the bottleneck using default settings. panic = "abort" reduces binary size and slightly improves performance by removing unwinding tables.