ullun-api
Express.js-inspired web framework for Rust — simple, fast, batteries-included
⚠️ 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 *;
async
Features
- 🚀 Fast: Built on hyper + tokio, measured numbers in PERFORMANCE.md
- 🎯 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, optionally with group-level middleware
- 📝 Forms:
application/x-www-form-urlencodedinto typed structs - 🖼️ Templates: Built-in dependency-free engine, or plug in your own via
TemplateEngine - 🚦 Rate limiting: In-memory middleware with
429/retry-after - 📖 OpenAPI 3.1: Document generated from your routes, optional Swagger UI
- 🧰 State & security helpers:
App::state/Request::state, CSPRNG tokens, constant-time comparison - 🔐 JWT (
jwtfeature), 🗜️ gzip (compressionfeature), 🔌 WebSockets (websocketfeature)
⚠️ Production Readiness
ullun-api is production-ready for small-to-medium APIs (<100k users), but requires proper deployment practices:
✅ Framework Provides
- Memory safety (zero
unsafecode) - Type-safe routing
- Graceful error handling
- CORS & security headers middleware
⚠️ User Must Implement
- Authentication/Authorization - Use middleware (see
examples/todo_api.rs) - Rate Limiting -
RateLimitermiddleware per instance; use a reverse proxy (nginx/Caddy) or a shared store for a global limit - 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
- ✅ Implement authentication middleware
- ✅ Use reverse proxy (nginx/Caddy) for TLS termination
- ✅ Add monitoring/observability (Prometheus, Sentry)
- ✅ Follow SECURITY.md checklist
- ✅ Load test your specific workload (
wrk,hey,ab) - ✅ Set up error logging and alerting
See SECURITY.md for comprehensive security guidelines.
Installation
[]
= "0.3"
= { = "1", = ["full"] }
= { = "1", = ["derive"] }
= "1"
Optional features (everything else is always on):
| Feature | Adds | Extra dependencies |
|---|---|---|
jwt |
ullun::jwt - HS256 tokens, JwtAuth middleware |
hmac, sha2, base64 |
compression |
ullun::compression::Compression - gzip middleware |
flate2 |
websocket |
App::ws, ullun::websocket |
tokio-tungstenite, futures-util |
full |
all of the above |
= { = "0.3", = ["jwt", "websocket"] }
Quick Start
Hello World
use *;
async
Path Parameters
new
.get_with_params
.run
.await
.unwrap;
JSON Request Body
async
new
.post
.run
.await
.unwrap;
Middleware
use ;
new
.middleware
.middleware
.get
.run
.await
.unwrap;
Route Groups (v0.2.0)
new
.group
.group
.run
.await?;
Cookie Handling (v0.2.0)
use ;
// Reading cookies
async
// Setting cookies
async
Static Files (v0.2.0)
new
.serve_static // Serve from 'public' directory
.serve_static // Serve from 'dist' at /assets
.run
.await?;
Body Size Limits (v0.2.0)
new
.max_body_size // 5 MB limit
// or
.max_body_size // Unlimited (not recommended)
.run
.await?;
Shared State
new
.state // one value per type, shared by all requests
.get
Security Helpers
use ;
let session_id = random_token?; // 128 bits from the OS CSPRNG, 32 hex chars
let short_code = random_alphanumeric?; // uniform [A-Za-z0-9]
let ok = constant_time_eq; // for API keys, delete tokens, ...
Sessions and CSRF
new
.middleware // cookie holds only an unguessable id
.middleware // POST/PUT/PATCH/DELETE need the session's token
.post
.get
SessionLayer::insecure_cookies() for plain-HTTP development, SessionLayer::with_store(..) for Redis or a database, Csrf::exempt(..) for bearer-token API calls. Anonymous visitors never receive a session cookie unless you write to the session.
File Uploads and Downloads
async
async
new
.max_body_size // small default ...
.body_limit // ... bigger where uploads happen
.serve_static // dotfiles hidden, traversal-proof
Uploaded HTML, SVG and scripts must never be served inline from your own origin: send them as attachment (see examples/drive).
HEAD, OPTIONS and 405
HEAD is answered by the GET handler (headers only). A known path with the wrong method gets 405 with an Allow header, and OPTIONS on a known path answers 204 with Allow. CORS preflights are still handled by the cors middleware.
Streaming and Server-Sent Events
use ;
async
Response::stream() gives the same for arbitrary bodies (downloads, exports): chunks are sent as they are produced, with back-pressure, and send fails once the client disconnects. ETags and Compression leave streams alone.
Background Tasks and Graceful Shutdown
new
.state
.background
.shutdown_grace // default 10 s
.run // stops on CTRL+C and SIGTERM
On shutdown the server stops accepting, closes idle keep-alive connections, lets running requests finish, signals Request::shutdown_signal() / Context::shutdown(), and waits for background tasks, each for at most the grace period. For embedding and tests use serve_with_shutdown(listener, signal).
Validation
use Validator;
let mut v = new;
v.len_between;
v.email;
v.range;
v.finish?; // 422 {"error":"Validation failed","fields":{"title":["..."],"priority":["..."]}}
All problems are reported at once. Typed path parameters: let id: u64 = req.params.parse("id")?; (400 naming the parameter).
ETags and optimistic concurrency
use ;
new
.middleware // ETag on GET responses, 304 for If-None-Match
.patch
Group Middleware
new
.middleware // every request, including 404s
.group
Forms
async
req.query.parse::<T>() converts numbers, booleans and options the same way.
Templates
let mut templates = new; // or Templates::from_dir("templates")?
templates.add?;
async
Variables are HTML-escaped ({{{raw}}} is not), with {{#if}} / {{else}} / {{#unless}}, {{#each}}, {{> partial}} and {{! comments }}. Using minijinja, tera or handlebars instead? Implement TemplateEngine (one method) and keep the same call sites.
Rate Limiting
use RateLimiter;
new
.middleware // per client IP
// .middleware(RateLimiter::new(5, Duration::from_secs(10)).key_by(|req| api_key(req)))
Answers 429 with retry-after and x-ratelimit-* headers. Behind a reverse proxy use .trust_forwarded_for(); never without one, because clients could pick their own bucket.
OpenAPI
use ;
new
.get_with_params
.doc
.openapi // /openapi.json + /docs
All routes are listed automatically, .doc adds the details. Swagger UI is opt-in because it loads a script from unpkg (pinned, with integrity hashes).
JWT (feature jwt)
let token = encode?;
new.group
HS256 only (alg: none and friends are rejected), constant-time verification, exp/nbf/issuer/audience checks, secrets of at least 32 bytes.
Compression (feature compression)
new.middleware // gzip text/JSON/XML/JS bodies >= 1 KiB when accepted
WebSockets (feature websocket)
new.ws
Cross-site Origins are rejected unless listed in WsOptions::allowed_origins (App::ws_with), messages are capped at 1 MiB.
API Reference
App
let app = new
.get // GET route
.post // POST route
.put // PUT route
.delete // DELETE route
.get_with_params // GET with path params extractor
.middleware // Add middleware
.run; // Start server (graceful shutdown on CTRL+C)
Need the bound port, or no signal handler (tests, benchmarks, embedding)? Bind yourself and use serve:
let listener = bind.await?;
println!;
app.serve.await?;
Middleware runs for every request, including unmatched routes (404) and CORS preflight (OPTIONS).
Handlers
Handlers can be:
- Async functions taking
Request→Result<Response> - Closures with path params:
Params→Result<Response>
// Full request access
async
// Path params only
.get_with_params
Error Handling
Errors automatically map to HTTP status codes:
return Err; // 400
return Err; // 401
return Err; // 403
return Err; // 404
return Err; // 500
return Err; // Custom
Middleware
Create custom middleware:
async
new
.middleware
// ...
Examples
See the examples/ directory. Basics:
hello_world.rs- Minimal examplefull_api.rs- CRUD API with middlewaretodo_api.rs- TODO API with token authenticationcomplete_api.rs- In-memory store, sessions, groupsfeatures_0_2_0.rs- Route groups, cookies, static files
Real-world applications
Complete services that run as they are and are covered by integration tests (tests/example_*.rs start the very same app module over real HTTP). Each lives in examples/<name>/ with a main.rs (configuration from the environment) and an app.rs (the testable application).
| App | Run | What it demonstrates |
|---|---|---|
shortener |
cargo run --example shortener |
URL shortener: API-key management API, public redirects, click counting, expiry, per-key rate limit, JSON file persistence, OpenAPI |
taskboard (jwt) |
DEV=1 cargo run --example taskboard --features jwt |
REST API: registration/login (argon2 + JWT), per-user projects and tasks, 422 field errors, filter/sort/pagination, ETag + If-Match, login throttling, OpenAPI + Swagger UI |
statuspage |
DEV=1 cargo run --example statuspage |
Uptime monitor: background scheduler, HTTP/TCP checks with SSRF protection, public status page with live Server-Sent Events, admin API, graceful SIGTERM shutdown |
blog |
DEV=1 cargo run --example blog |
Blog/CMS: Markdown posts (escaped HTML, safe links), admin login with argon2, sessions renewed on login, CSRF on every form, throttled login, strict CSP, RSS and sitemap, drafts, ETags |
drive |
DEV=1 cargo run --example drive |
File drive: multipart uploads, per-user quotas, resumable downloads (Range), expiring share links with download limits, sandboxed risky types, per-route body limits |
pastebin |
cargo run --example pastebin |
Paste service: HTML form + JSON API, escaped views with CSP, expiry, burn-after-read, delete tokens, storage cap, shared rate-limit budget |
Scenarios (each is a self-contained deployment shape):
| Scenario | Example | Shows |
|---|---|---|
| Containerised microservice | microservice.rs |
/healthz + /readyz probes, request ids, Prometheus metrics, PORT config, graceful shutdown |
| Public API for third parties | public_api.rs |
API keys, per-key RateLimiter, CORS preflight, versioned group, typed query pagination, OpenAPI |
| SPA + backend in one binary | spa_backend.rs |
Static frontend at /, JSON API under /api, security headers |
| Webhook receiver | webhook_receiver.rs |
Constant-time token check, body limit, idempotency, 202 + background worker |
| Server-rendered HTML | html_forms.rs |
Form posts, cookie sessions, flash messages, Post/Redirect/Get, HTML escaping |
| Token-secured API | jwt_auth.rs (jwt) |
Login, bearer-protected group, roles, OpenAPI with security scheme and Swagger UI |
| Real-time app | realtime_chat.rs (websocket) |
Broadcast chat, origin check, browser client |
| Templated site | templates_site.rs (compression) |
Template engine, validated forms, gzip, JSON twin of the page |
Run examples:
PORT=8080
Test the API:
# GET /
# GET with path params
# POST with JSON
Comparison
| Framework | Learning Curve | Middleware | Style |
|---|---|---|---|
| Actix-web | High | Complex | Actor/extractor based |
| Rocket | Medium | Fairings | Macro/attribute based |
| Axum | Medium | Tower layers | Extractor based |
| ullun-api | Low | Express-style (req, next) |
Closures and async fn(Request) |
This table is qualitative. We do not publish cross-framework speed claims; see PERFORMANCE.md for what is measured.
Testing & Benchmarks
# Run tests
# Run benchmarks (micro + end-to-end HTTP)
# Check coverage
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
Done
- Route groups, cookies, static file serving, body size limits (0.2)
- Scenario examples, end-to-end HTTP benchmarks
- Group-level middleware, form parsing, template engine integration
- Rate limiting, JWT, gzip compression, WebSockets, OpenAPI generation
Ideas
- OpenAPI schemas derived from types (needs a derive macro or
schemars) -
multipart/form-datauploads -
HEAD/OPTIONShandling in the router - Shared rate-limit stores (Redis) behind a trait
- More JWT algorithms (RS256/ES256) and JWKS
CI/CD
GitHub Actions (.github/workflows/ci.yml):
- ✅ Lint:
rustfmt,clippy -D warnings, rustdoc,cargo auditon every PR and onmain - ✅ Test: Linux, macOS, Windows
- ✅ Build: release build of all examples, benchmarks compile
- ✅ Commits: PR title and commits must be Conventional Commits
- ✅ Release: on
main, only after lint, tests and build passed, release-plz derives the next version from the commits, maintains a release PR with the changelog, and on merge tags, creates the GitHub release and publishes to crates.io
See CONTRIBUTING.md for the details and required secrets.
Contributing
Contributions welcome! Commits follow Conventional Commits; see CONTRIBUTING.md for guidelines.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Documentation
- Architecture Guide - Technical deep-dive
- Deployment Guide - Production deployment
- Testing Guide - Testing strategies
- Performance Guide - Optimization tips
- Security Policy - Security best practices
Acknowledgments
- Built on hyper and tokio
- Routing by matchit
- Inspired by Express.js
Made with ❤️ by yedoma-labs