chopin

The Chopin Framework: Comprehensive API Development Guide

Welcome to the official developer guide for the Chopin HTTP Framework. This guide is designed to take you from a hello-world API all the way to a production-hardened, database-backed microservice, leveraging Chopin’s unique Shared-Nothing architecture.


1. Foundation & Setup

Environment Configuration

Chopin requires a modern Rust toolchain (1.75+). It’s optimized for UNIX-like environments (Linux epoll and macOS kqueue).

Install the CLI Toolkit: The chopin-cli is your central tool for generating projects and managing them:

cargo install --path crates/chopin-cli

Project Initialization: Create your new project with:

chopin new my_api
cd my_api
chopin dev

The Big Picture (Architecture)

Chopin is not like Actix or Axum; it eschews heavy async runtimes (like Tokio) for the critical request path. It relies on a Shared-Nothing threaded model:

Boilerplate Breakdown

When you run chopin new, you get:


2. Core Routing & Handlers

Request Mapping

Chopin uses declarative attribute macros (#[get], #[post], #[put], #[delete]). Handlers take a single Context argument and return a Response.

use chopin_core::{Context, Response};
use chopin_macros::get;

#[get("/ping")]
fn ping(ctx: Context) -> Response {
    Response::text("pong")
}

Path Parameters & Headers

URL path variables and HTTP headers can be cleanly extracted from the Context.

#[post("/users/:id")]
fn update_user(ctx: Context) -> Response {
    let user_id = ctx.param("id").unwrap_or("0");
    let auth_header = ctx.header("Authorization");
    
    Response::text(format!("Updating {}", user_id))
}

Type-Safe JSON Request Bodies

Chopin incorporates kowito-json, an ultra-fast Schema-JIT (Just-In-Time) serializer. It replaces standard reflection with compile-time layout maps.

use kowito_json::serialize::Serialize; // Or use chopin_core::json::Serialize

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

#[get("/user")]
fn get_user(ctx: Context) -> Response {
    let payload = UserResponse { id: 1, username: "virtuoso".into() };
    ctx.json(&payload)
}

Zero-Copy Static File Serving

Response::file(path) opens a file and serves it via the platform sendfile syscall, so the file contents are transferred entirely in kernel space — Chopin’s user-space process never touches the bytes.

#[get("/assets/:name")]
fn serve_asset(ctx: Context) -> Response {
    let name = ctx.param("name").unwrap_or("");
    Response::file(&format!("public/{}", name))
    // Automatically sets Content-Type from extension (~30 MIME types)
    // Returns 404 if the file doesn't exist
}

For custom byte ranges or pre-opened file descriptors, use Response::sendfile(fd, offset, len, content_type).


3. The Middleware Pipeline

Middlewares in Chopin are pure functions that take a request Context and a BoxedHandler (the next step in the request chain) and return a Response.

Interceptors

fn timing_middleware(ctx: Context, next: BoxedHandler) -> Response {
    let start = std::time::Instant::now();
    let mut response = next(ctx);
    let duration = start.elapsed();
    
    response.headers.push(("X-Runtime-Ms", duration.as_millis().to_string()));
    response
}

You attach these globally via Router::new().layer(timing_middleware).

Authentication & Role-Based Authorization

The chopin-auth crate brings JWT validation. Middleware can be statically generated with zero allocations using the require_role_middleware! macro.

use chopin_auth::{require_role_middleware, Role};

#[derive(PartialEq, Role)]
pub enum UserRole { Admin, Guest }

// This generates a static function `admin_only`
require_role_middleware!(admin_only, MyClaims, UserRole::Admin, MyClaims::has_role);

fn main() {
    let mut router = Router::new();
    // Wrap handler locally
    router.get("/admin/dashboard", admin_only(dashboard_handler));
}

4. Data & Persistence

Chopin implements a natively synchronous, high-throughput Postgres connection via chopin-pg, overlaid with the zero-allocation chopin-orm.

Worker-Local Pooling

Because Chopin is Shared-Nothing, you should NOT use an Arc<Mutex<Pool>>. Use chopin_pg::init_pool + Chopin::with_worker_init — each worker thread gets its own isolated PgPool with zero synchronisation overhead.

fn main() {
    Chopin::new()
        .mount_all_routes()
        .with_worker_init(|| {
            chopin_pg::init_pool(
                "postgres://user:pass@localhost/main",
                10, // connections PER WORKER
            ).expect("DB pool init failed");
        })
        .serve("0.0.0.0:8080")
        .unwrap();
}

Inside any handler, call chopin_pg::pool() — no argument passing or DB.with closures needed:

#[get("/products/:id")]
fn show_product(ctx: Context) -> Response {
    let id: i32 = match ctx.param_parse("id") {
        Ok(v)  => v,
        Err(r) => return r,
    };
    match Product::find_by_id(chopin_pg::pool(), id) {
        Ok(Some(p)) => Response::json(&p),
        Ok(None)    => Response::new(404),
        Err(_)      => Response::new(500),
    }
}

ORM & ActiveModel

Declaring models and interacting with the database is completely type-safe with the Model derive macro.

ActiveModel for Partial Updates: For fine-grained control over updates, use ActiveModel. It tracks modified fields dynamically, ensuring only changed columns are sent to the database.

#[get("/products/:id")]
fn update_price(ctx: Context) -> Response {
    let id: i32 = match ctx.param_parse("id") {
        Ok(v)  => v,
        Err(r) => return r,
    };
    let pool = chopin_pg::pool();

    // 1. Fetch existing model
    let product = match Product::find_by_id(pool, id) {
        Ok(Some(p)) => p,
        Ok(None)    => return Response::new(404),
        Err(_)      => return Response::new(500),
    };

    // 2. Wrap in ActiveModel
    let mut active = ProductActiveModel::from(product);

    // 3. Set ONLY the fields that changed
    active.set("price", 4000);

    // 4. Save - intelligently issues UPDATE or INSERT
    match active.save(pool) {
        Ok(_)  => Response::new(200),
        Err(_) => Response::new(500),
    }
}

Advanced Relationship Joins

Chopin ORM supports automatic eager loading and joins, including tables with composite primary keys.

Defining Relations: Use belongs_to and has_many attributes in your model definition.

#[derive(Model)]
struct Order {
    #[model(primary_key)]
    id: i32,
    #[model(belongs_to = User)]
    user_id: i32,
}

Eager Loading with Joins:

let orders_with_users = Order::find()
    .join_parent::<User>()
    .all(&mut *pool).unwrap();

Chopin automatically resolves the foreign key mapping and constructs the JOIN clause.


5. Advanced Logic

Streaming File Uploads (Multipart)

Chopin provides an inherently streaming Multipart parser on the Context that steps through body bytes without allocating massive buffers, protecting your application from out-of-memory errors on large uploads.

#[post("/upload")]
fn upload(ctx: Context) -> Response {
    if let Some(multipart) = ctx.multipart() {
        for part in multipart {
            let p = part.unwrap();
            println!("File Name: {:?}", p.filename);
            // Slice of bytes extracted immediately: p.body
        }
    }
    Response::text("Upload Done")
}

Global Error Handling (Catch-Unwind)

By default, Chopin captures worker panics via catch_unwind. A faulty handler route that triggers a panic will fail transparently gracefully with a 500 Internal Server Error, keeping the worker loop and connection handlers actively evaluating the next request.


6. Performance & Scaling

Chopin operates efficiently on a single core but shines when scaling across physical CPUs.

Thread-Per-Core Strategy & Affinity

In its initialization sequence, Chopin discovers the logical core count. Workers are explicitly pinned using core_affinity. Memory stays hot in L1/L2 caches.

Zero-Copy I/O

Two techniques eliminate unnecessary data copies on every response:

Pre-Composed Middleware

At startup, Router::finalize() walks the entire route tree and composes all middleware chains into a single Arc<dyn Fn(Context) -> Response> per route. On the hot path, Chopin calls one pre-built closure — no Arc::new, no chain construction, no allocations.

Global Allocator: mimalloc

Chopin uses mimalloc as the global allocator. Under heavy concurrency, mimalloc delivers dramatically lower allocation latency than the system allocator by using per-thread free lists and avoiding global lock contention.

Kernel-Level Socket Handoff

Chopin exploits the deep optimizations in Linux (TCP_DEFER_ACCEPT, TCP_FASTOPEN) and macOS (SO_NOSIGPIPE, TCP_FASTOPEN). TCP_NODELAY is attached immediately to the initial listener so all accepted connections natively inherit the flag, preventing an O(N) penalty dynamically modifying individual sockets.


7. Testing & Quality

The Architectural Linter

Validation is backed natively by the CLI tool:

chopin check

This utility analyzes standard project files and blocks builds containing prohibited anti-patterns (such as importing heavy async frameworks out-of-band).

Benchmarking

Before deploying, validate your API throughput locally using wrk:

chopin bench
# Example: 10 threads, 200 connections
wrk -t10 -c200 -d10s http://localhost:8080/ping

8. Deployment & Observability

Containerization

Standard setups include a pre-optimized, multi-stage Docker build ready out of the box with chopin-cli deployments.

chopin deploy docker

This scaffolding generates amd64 / arm64 tuned deployment images leveraging musl compilation.

Prometheus Metrics

Mount a Prometheus-compatible scrape endpoint and a JSON health probe:

Chopin::new()
    .mount_all_routes()
    .with_metrics("/metrics")   // GET /metrics → Prometheus text format
    .with_health("/health")     // GET /health  → JSON for k8s / ALB probes
    .serve("0.0.0.0:8080")
    .unwrap();

Exposes chopin_requests_total, chopin_active_connections, chopin_bytes_sent_total, chopin_uptime_seconds.

Structured JSON Logging

Enable the logging feature and call .with_logging() before .serve():

# Cargo.toml
chopin-core = { version = "0.5.27", features = ["logging"] }
RUST_LOG=info cargo run

TLS / HTTPS

Enable the tls feature to get in-worker rustls TLS 1.2/1.3 termination:

# Cargo.toml
chopin-core = { version = "0.5.27", features = ["tls"] }
Chopin::new()
    .mount_all_routes()
    .serve_tls("0.0.0.0:443", "certs/cert.pem", "certs/key.pem")
    .unwrap();

End of Guide.