# ullun-api
> **Express.js-inspired web framework for Rust** — simple, fast, batteries-included
[](https://github.com/yedoma-labs/ullun-api/actions/workflows/ci.yml)
[](https://github.com/yedoma-labs/ullun-api/actions)
[](https://crates.io/crates/ullun-api)
[](https://crates.io/crates/ullun-api)
[](https://docs.rs/ullun-api)
[](LICENSE)
[](https://www.rust-lang.org)
[](https://codecov.io/gh/yedoma-labs/ullun-api)
[](src/)
[](#security)
[](docs/PERFORMANCE.md)
[](#features)
[](https://github.com/yedoma-labs/ullun-api)
[](https://github.com/yedoma-labs/ullun-api/fork)
[](https://github.com/yedoma-labs/ullun-api/issues)
[](https://github.com/yedoma-labs/ullun-api/graphs/contributors)
> ⚠️ **Production Note:** Requires proper deployment (TLS, auth, rate limiting). See [Production Readiness](#%EF%B8%8F-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.
```rust
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](SECURITY.md) checklist
5. ✅ Load test your specific workload (`wrk`, `hey`, `ab`)
6. ✅ Set up error logging and alerting
**See [SECURITY.md](SECURITY.md) for comprehensive security guidelines.**
---
## Installation
```toml
[dependencies]
ullun-api = "0.2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```
## Quick Start
### Hello World
```rust
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
```rust
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
```rust
#[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
```rust
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)
```rust
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)
```rust
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)
```rust
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)
```rust
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
```rust
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>`
```rust
// Full request access
async fn handler(req: Request) -> Result<Response> {
let body = req.json::<MyType>()?;
Ok(Response::json(body))
}
// Path params only
// ...
})
```
### Error Handling
Errors automatically map to HTTP status codes:
```rust
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:
```rust
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/`](examples/) directory:
- [`hello_world.rs`](examples/hello_world.rs) - Minimal example
- [`full_api.rs`](examples/full_api.rs) - Full CRUD API with middleware
- [`todo_api.rs`](examples/todo_api.rs) - Complete TODO API with authentication
Run examples:
```bash
cargo run --example hello_world
cargo run --example full_api
cargo run --example todo_api
```
Test the API:
```bash
# 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
| Actix-web | 500KB | High | Complex | ★★★★☆ |
| Rocket | 600KB | Medium | Good | ★★★★★ |
| Axum | 300KB | Medium | Tokio-coupled | ★★★★☆ |
| **ullun-api** | ~150KB | **Low** | **Express-style** | ★★★★★ |
## Testing & Benchmarks
```bash
# Run tests
cargo test
# Run benchmarks
cargo bench
# Check coverage
cargo tarpaulin --out Html
```
See [TESTING.md](docs/TESTING.md) and [PERFORMANCE.md](docs/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](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/`](.github/workflows/) for details.
## Contributing
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Documentation
- [Architecture Guide](docs/ARCHITECTURE.md) - Technical deep-dive
- [Deployment Guide](docs/DEPLOYMENT.md) - Production deployment
- [Testing Guide](docs/TESTING.md) - Testing strategies
- [Performance Guide](docs/PERFORMANCE.md) - Optimization tips
- [Security Policy](SECURITY.md) - Security best practices
## Acknowledgments
- Built on [hyper](https://hyper.rs/) and [tokio](https://tokio.rs/)
- Routing by [matchit](https://github.com/ibraheemdev/matchit)
- Inspired by [Express.js](https://expressjs.com/)
---
**Made with ❤️ by [yedoma-labs](https://github.com/yedoma-labs)**