ullun-api 0.2.0

Express.js-inspired web framework for Rust - simple, fast, batteries-included
Documentation

ullun-api

Express.js-inspired web framework for Rust — simple, fast, batteries-included

CI Security Audit Crates.io Downloads Documentation License Rust Version

codecov Lines of Code No Unsafe Performance Binary Size

GitHub stars GitHub forks GitHub issues GitHub contributors

⚠️ Production Note: Requires proper deployment (TLS, auth, rate limiting). See Production Readiness below.

ullun (уллун; Yakutian/Sakha) = "large/great" — powerful yet simple

Why ullun-api?

The Problem: Actix-web is fast but complex. Rocket is ergonomic but heavy. Axum ties you to Tokio internals.

The Solution: An Express.js-style framework for Rust — single-file APIs, minimal boilerplate, maximum performance.

use ullun::prelude::*;

#[tokio::main]
async fn main() {
    App::new()
        .get("/", |_req| async { Ok(Response::text("Hello, World!")) })
        .get_with_params("/hello/{name}", |params| async move {
            Ok(Response::json(serde_json::json!({
                "message": format!("Hello, {}!", params.get("name")?)
            })))
        })
        .run("127.0.0.1:3000")
        .await
        .unwrap();
}

Features

  • 🚀 Fast: Built on hyper + tokio, ~150KB compiled binary
  • 🎯 Simple: Express.js-style routing, zero boilerplate
  • 🔒 Type-safe: Path parameters validated at compile-time
  • 🔗 Middleware: Chain middleware like Express.js
  • 📦 Batteries-included: JSON serialization, error handling, CORS built-in
  • 🎨 Ergonomic: Result<T, E> maps to HTTP status codes automatically
  • 🍪 Cookie Support: Parse and set HTTP cookies with ease
  • 📁 Static Files: Serve static files from directories
  • 🎯 Route Groups: Organize routes with common prefixes

⚠️ Production Readiness

ullun-api is production-ready for small-to-medium APIs (<100k users), but requires proper deployment practices:

✅ Framework Provides

  • Memory safety (zero unsafe code)
  • Type-safe routing
  • Graceful error handling
  • CORS & security headers middleware

⚠️ User Must Implement

  • Authentication/Authorization - Use middleware (see examples/todo_api.rs)
  • Rate Limiting - Use reverse proxy (nginx/Caddy) or custom middleware
  • TLS/HTTPS - Use reverse proxy (never expose raw HTTP in production)
  • Input Validation - Validate business logic (framework validates types)
  • SQL Injection Prevention - Use parameterized queries (sqlx, diesel)
  • XSS Protection - Sanitize HTML output (if serving HTML)

📋 Pre-Production Checklist

  1. ✅ Implement authentication middleware
  2. ✅ Use reverse proxy (nginx/Caddy) for TLS termination
  3. ✅ Add monitoring/observability (Prometheus, Sentry)
  4. ✅ Follow SECURITY.md checklist
  5. ✅ Load test your specific workload (wrk, hey, ab)
  6. ✅ Set up error logging and alerting

See SECURITY.md for comprehensive security guidelines.


Installation

[dependencies]
ullun-api = "0.2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Quick Start

Hello World

use ullun::prelude::*;

#[tokio::main]
async fn main() {
    App::new()
        .get("/", |_req| async {
            Ok(Response::text("Hello, World!"))
        })
        .run("127.0.0.1:3000")
        .await
        .unwrap();
}

Path Parameters

App::new()
    .get_with_params("/users/{id}", |params| async move {
        let id: u64 = params.get("id")?.parse()
            .map_err(|_| Error::bad_request("Invalid user ID"))?;
        
        Ok(Response::json(serde_json::json!({
            "user_id": id
        })))
    })
    .run("127.0.0.1:3000")
    .await
    .unwrap();

JSON Request Body

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

async fn create_user(req: Request) -> Result<Response> {
    let body: CreateUser = req.json()?;
    
    Ok(Response::json(serde_json::json!({
        "message": format!("Created user: {}", body.name)
    })))
}

App::new()
    .post("/users", create_user)
    .run("127.0.0.1:3000")
    .await
    .unwrap();

Middleware

use ullun::middleware::{logger, cors};

App::new()
    .middleware(logger)
    .middleware(cors(vec!["*".to_string()]))
    .get("/", |_req| async { Ok(Response::text("Hello!")) })
    .run("127.0.0.1:3000")
    .await
    .unwrap();

Route Groups (v0.2.0)

App::new()
    .group("/api", |group| {
        group
            .get("/users", list_users)
            .post("/users", create_user)
    })
    .group("/admin", |group| {
        group
            .get("/dashboard", admin_dashboard)
            .post("/settings", update_settings)
    })
    .run("127.0.0.1:3000")
    .await?;

Cookie Handling (v0.2.0)

use ullun::cookies::{Cookie, SameSite};

// Reading cookies
async fn get_session(req: Request) -> Result<Response> {
    let cookies = req.cookies();
    if let Some(session_id) = cookies.get("session") {
        Ok(Response::text(format!("Session: {}", session_id)))
    } else {
        Err(Error::unauthorized("Not logged in"))
    }
}

// Setting cookies
async fn login(_req: Request) -> Result<Response> {
    let cookie = Cookie::new("session", "abc123")
        .path("/")
        .max_age(3600)
        .http_only()
        .secure()
        .same_site(SameSite::Lax);
    
    Ok(Response::text("Logged in").cookie(cookie))
}

Static Files (v0.2.0)

App::new()
    .serve_static("/", "public")        // Serve from 'public' directory
    .serve_static("/assets", "dist")    // Serve from 'dist' at /assets
    .run("127.0.0.1:3000")
    .await?;

Body Size Limits (v0.2.0)

App::new()
    .max_body_size(Some(5 * 1024 * 1024))  // 5 MB limit
    // or
    .max_body_size(None)                    // Unlimited (not recommended)
    .run("127.0.0.1:3000")
    .await?;

API Reference

App

let app = App::new()
    .get(path, handler)           // GET route
    .post(path, handler)          // POST route
    .put(path, handler)           // PUT route
    .delete(path, handler)        // DELETE route
    .get_with_params(path, handler) // GET with path params extractor
    .middleware(middleware)       // Add middleware
    .run(addr);                   // Start server

Handlers

Handlers can be:

  • Async functions taking Request → Result<Response>
  • Closures with path params: Params → Result<Response>
// Full request access
async fn handler(req: Request) -> Result<Response> {
    let body = req.json::<MyType>()?;
    Ok(Response::json(body))
}

// Path params only
.get_with_params("/users/{id}", |params| async move {
    let id = params.get("id")?;
    // ...
})

Error Handling

Errors automatically map to HTTP status codes:

return Err(Error::bad_request("Invalid input"));      // 400
return Err(Error::unauthorized("Login required"));    // 401
return Err(Error::forbidden("Access denied"));        // 403
return Err(Error::not_found("User not found"));       // 404
return Err(Error::internal("Database error"));        // 500
return Err(Error::custom(418, "I'm a teapot"));       // Custom

Middleware

Create custom middleware:

async fn my_middleware(req: Request, next: Next) -> Result<Response> {
    // Before handler
    println!("Before: {}", req.uri);
    
    let response = next.run(req).await?;
    
    // After handler
    println!("After: {}", response.status);
    
    Ok(response)
}

App::new()
    .middleware(my_middleware)
    // ...

Examples

See the examples/ directory:

Run examples:

cargo run --example hello_world
cargo run --example full_api
cargo run --example todo_api

Test the API:

# GET /
curl http://localhost:3000/

# GET with path params
curl http://localhost:3000/hello/world

# POST with JSON
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com"}'

Comparison

Framework Bundle Size Learning Curve Middleware Type Safety
Actix-web 500KB High Complex ★★★★☆
Rocket 600KB Medium Good ★★★★★
Axum 300KB Medium Tokio-coupled ★★★★☆
ullun-api ~150KB Low Express-style ★★★★★

Testing & Benchmarks

# Run tests
cargo test

# Run benchmarks
cargo bench

# Check coverage
cargo tarpaulin --out Html

See TESTING.md and PERFORMANCE.md for details.

Security

✅ No unsafe code
✅ Memory safe (Rust guarantees)
✅ No unwrap() in production paths
✅ Header validation (prevents malformed headers)
✅ Graceful error handling (no panics)

⚠️ User Responsibilities:

  • Authentication/authorization implementation
  • Rate limiting (reverse proxy or middleware)
  • Input validation (business logic)
  • SQL injection prevention (parameterized queries)
  • XSS protection (if serving HTML)

See SECURITY.md for comprehensive guidelines and best practices.

Roadmap

v0.2

  • OpenAPI/Swagger generation
  • WebSocket support
  • Static file serving
  • Form data parsing
  • Cookie handling

v0.3+

  • Template engine integration
  • Database connection pooling helpers
  • Rate limiting middleware
  • JWT authentication helpers
  • Compression middleware

CI/CD

GitHub Actions workflows:

  • ✅ CI: Test on Linux/macOS/Windows + Rust stable/beta
  • ✅ Security: cargo-audit checks
  • ✅ Format: rustfmt checks
  • ✅ Lint: clippy checks
  • ✅ Coverage: Code coverage reports
  • ✅ Benchmarks: Performance tracking
  • ✅ Release: Automated builds & publishing

See .github/workflows/ for details.

Contributing

Contributions welcome! See CONTRIBUTING.md for guidelines.

License

Licensed under either of:

at your option.

Documentation

Acknowledgments


Made with ❤️ by yedoma-labs