API Reference
Complete API reference for the Ultimo web framework.
Core Types
Ultimo
The main application struct that manages routing, middleware, and server lifecycle.
use ultimo::prelude::*;
let mut app = Ultimo::new();Methods
new() -> Self
Create a new Ultimo application with default middleware (includes X-Powered-By: Ultimo header).
let mut app = Ultimo::new();new_without_defaults() -> Self
Create a new Ultimo application without any default middleware. Use this for full control over middleware configuration.
let mut app = Ultimo::new_without_defaults();Routing Methods
Register route handlers for different HTTP methods:
// GET route
app.get("/path", handler);
// POST route
app.post("/path", handler);
// PUT route
app.put("/path", handler);
// DELETE route
app.delete("/path", handler);
// PATCH route
app.patch("/path", handler);use_middleware(&mut self, middleware: impl IntoMiddleware) -> &mut Self
Add middleware to the application. Middleware executes in the order it's added.
app.use_middleware(ultimo::middleware::builtin::logger());
app.use_middleware(ultimo::middleware::builtin::cors());listen(&mut self, addr: &str) -> Result<()>
Start the HTTP server on the specified address.
app.listen("127.0.0.1:3000").await?;max_body_size(&mut self, bytes: usize) -> &mut Self
Reject requests whose body exceeds bytes with 413 Payload Too Large (the
oversized body is never fully buffered). No limit by default.
app.max_body_size(2 * 1024 * 1024); // 2 MBtrust_proxy(&mut self, trust: bool) -> &mut Self
Trust X-Forwarded-For / Forwarded headers for Context::client_ip.
Only enable behind a trusted proxy (these headers are client-spoofable).
Defaults to false.
app.trust_proxy(true);serve_static(&mut self, prefix: &str, dir: &str) (requires static-files feature)
Register a GET {prefix}/* route that reads files from dir on disk. Sets
Content-Type, ETag, and Content-Length automatically; returns 304 Not Modified when If-None-Match matches. Rejects path traversal attempts.
// GET /assets/style.css → reads ./public/style.css
app.serve_static("/assets", "./public");See Static Files.
serve_spa(&mut self, dir: &str, fallback: &str) (requires static-files feature)
Configure an SPA fallback: any GET request that doesn't match a registered
route returns the specified file from dir. POST / PUT / DELETE etc. 404s
are not intercepted.
app.serve_spa("./dist", "index.html");See Static Files.
serve_docs(&mut self, path: &str, spec: OpenApiSpec)
Serve interactive API documentation (Swagger UI) at the given path. Registers
two routes: GET {path} (Swagger UI page) and GET {path}/openapi.json (the
spec). One-liner equivalent of FastAPI's /docs.
use ultimo::openapi::OpenApiBuilder;
let spec = OpenApiBuilder::new()
.title("My API")
.version("1.0.0")
.build();
app.serve_docs("/docs", spec);See OpenAPI.
oneshot(&self, req: hyper::Request<Full<Bytes>>) -> Response
Dispatch a fully-buffered request through the app in-process (no socket). The seam the testing utilities build on; handy for embedding.
Database Methods (requires feature flags)
With SQLx (requires sqlx feature):
app.with_sqlx(pool);With Diesel (requires diesel feature):
app.with_diesel(pool);Session middleware (requires session feature)
use ultimo::session::{session, MemoryStore, SessionConfig};
app.use_middleware(session(MemoryStore::new(), SessionConfig::default()));See Sessions.
Context & Request
Context
The request context provides access to the incoming request and allows building responses.
async fn handler(ctx: Context) -> Result<Response> {
// Access request data through ctx.req
// Build responses with ctx methods
ctx.json(json!({"message": "Hello"})).await
}Response Methods
json<T: Serialize>(&self, value: T) -> Result<Response>
Return a JSON response with Content-Type: application/json.
ctx.json(json!({"key": "value"})).awaittext(&self, body: impl Into<String>) -> Result<Response>
Return a plain text response with Content-Type: text/plain.
ctx.text("Hello, World!").awaithtml(&self, body: impl Into<String>) -> Result<Response>
Return an HTML response with Content-Type: text/html.
ctx.html("<h1>Hello</h1>").awaitredirect(&self, location: &str) -> Result<Response>
Return a 302 redirect response.
ctx.redirect("/new-path").awaitstatus(&self, code: u16)
Set the response status code. Can be chained with other response methods.
ctx.status(201);
ctx.json(user).awaitheader(&self, key: &str, value: &str)
Set a response header. Can be chained with other response methods.
ctx.header("X-Custom", "value");
ctx.json(data).awaitCookies
use ultimo::cookie::{Cookie, SameSite};
// Read a request cookie
let theme = ctx.cookie("theme"); // Option<String>
let all = ctx.cookies(); // HashMap<String, String>
// Set / remove a response cookie
ctx.set_cookie(Cookie::new("theme", "dark").http_only(true).same_site(SameSite::Lax)).await?;
ctx.remove_cookie("theme").await?;Client IP
client_ip(&self) -> Option<IpAddr> · peer_addr(&self) -> Option<SocketAddr>
client_ip() is the best-effort originating client — honors X-Forwarded-For /
Forwarded only when app.trust_proxy(true),
else the connection peer. peer_addr() is the raw connection peer. See
Security.
let ip = ctx.client_ip();Session (requires session feature)
session(&self) -> Session
The current session (panics if the session middleware isn't installed).
ctx.session().await.set("user_id", &42u64).await?;
let id: Option<u64> = ctx.session().await.get("user_id").await?;See Sessions.
JWT claims (requires jwt feature)
jwt_claims(&self) -> Option<serde_json::Value>
The validated JWT claims for this request (None if no valid token was presented).
jwt<T: DeserializeOwned>(&self) -> Result<T>
Deserialize the validated claims into a typed struct (errors if absent or shape mismatch).
let claims = ctx.jwt_claims().await; // Option<serde_json::Value>
let mine: MyClaims = ctx.jwt::<MyClaims>().await?;See JWT Authentication.
API-key identity (requires api-key feature)
api_key(&self) -> Option<ApiKeyIdentity>
The API-key identity for this request (id + scopes), or None if no valid
key was presented. See API-Key Authentication.
if let Some(identity) = ctx.api_key().await {
// identity.id, identity.scopes
}Authorization guards (requires jwt or api-key feature)
Both auth middlewares populate a normalized auth::Principal { id, scopes },
which these guards read. They compose with ? and short-circuit with 401/403.
principal(&self) -> Option<Principal>
The normalized authenticated caller, if any.
require_auth(&self) -> Result<Principal>
The Principal, or 401 Unauthorized.
require_scope(&self, scope: &str) -> Result<()>
401 if unauthenticated, 403 Forbidden if the scope is missing.
require_any_scope(&self, scopes: &[&str]) -> Result<()> · require_all_scopes(&self, scopes: &[&str]) -> Result<()>
403 unless (respectively) at least one / all of the scopes are present.
app.get("/admin", |ctx: Context| async move {
ctx.require_scope("admin").await?;
ctx.json(json!({ "ok": true })).await
});Request
Access request data through ctx.req.
Path Parameters
param(&self, name: &str) -> Result<&str>
Get a path parameter by name.
// Route: /users/:id
let id = ctx.req.param("id")?;params(&self) -> &Params
Get all path parameters as a HashMap.
let all_params = ctx.req.params();Query Parameters
query(&self, name: &str) -> Option<String>
Get a single query parameter.
// GET /search?q=rust
let query = ctx.req.query("q");queries(&self) -> HashMap<String, Vec<String>>
Get all query parameters. Returns a map of parameter names to their values (supports multiple values per parameter).
let all_queries = ctx.req.queries();Headers
header(&self, name: &str) -> Option<String>
Get a request header value.
let auth = ctx.req.header("Authorization");headers(&self) -> &HeaderMap
Get all request headers.
let all_headers = ctx.req.headers();Body
json<T: DeserializeOwned>(&self) -> Result<T>
Parse the request body as JSON.
let body: CreateUser = ctx.req.json().await?;text(&self) -> Result<String>
Get the request body as a string.
let body = ctx.req.text().await?;bytes(&self) -> Result<Bytes> · raw_body(&self) -> Result<Bytes>
Get the request body as raw bytes. The body is buffered and cached, so
json/text/bytes/raw_body may be called multiple times.
let body = ctx.req.bytes().await?;Method & URI
method(&self) -> &Method
Get the HTTP method (GET, POST, etc.).
let method = ctx.req.method();uri(&self) -> &Uri
Get the request URI.
let uri = ctx.req.uri();
let path = uri.path();RPC System
RpcRegistry
Registry for RPC procedures with automatic TypeScript generation.
Creating a Registry
use ultimo::rpc::{RpcRegistry, RpcMode};
// JSON-RPC mode (default)
let rpc = RpcRegistry::new();
// REST mode
let rpc = RpcRegistry::new_with_mode(RpcMode::Rest);Registering Procedures
query<F, I, O>(&self, name: &str, handler: F)
Register a query procedure (idempotent, read-only operations).
- REST mode: Maps to GET endpoint
- JSON-RPC mode: Part of RPC protocol
rpc.query("getUser", |input: GetUserInput| async move {
Ok(User { /* ... */ })
});mutation<F, I, O>(&self, name: &str, handler: F)
Register a mutation procedure (non-idempotent operations that modify state).
- REST mode: Maps to POST endpoint
- JSON-RPC mode: Part of RPC protocol
rpc.mutation("createUser", |input: CreateUserInput| async move {
Ok(User { /* ... */ })
});register_with_types<F, I, O>(&self, name: &str, handler: F, input_type: String, output_type: String)
Register a procedure with explicit TypeScript type annotations.
rpc.register_with_types(
"getUser",
|input: GetUserInput| async move { Ok(User { /* ... */ }) },
"{ id: number }".to_string(),
"User".to_string(),
);TypeScript Generation
generate_client_file(&self, path: &str) -> Result<()>
Generate a TypeScript client file with type-safe methods.
rpc.generate_client_file("../frontend/src/lib/client.ts")?;generate_client(&self) -> String
Generate TypeScript client code as a string.
let ts_code = rpc.generate_client();RPC Modes
pub enum RpcMode {
/// Each procedure becomes its own HTTP endpoint
/// - Queries: GET /api/{name}
/// - Mutations: POST /api/{name}
Rest,
/// All procedures use a single POST endpoint
/// POST /rpc with JSON-RPC 2.0 protocol
JsonRpc,
}JSON-RPC 2.0 Protocol
handle_request(&self, body: &[u8]) -> JsonRpcOutput
Full JSON-RPC 2.0 dispatch. Parses the body, validates, dispatches (concurrently for batch), and returns spec-compliant responses.
let body = ctx.req.bytes().await?;
let output = registry.handle_request(&body).await;
match output.into_body() {
Some(bytes) => {
let value: serde_json::Value = serde_json::from_slice(&bytes)?;
ctx.json(value).await
}
None => {
ctx.status(204).await;
ctx.text("").await
}
}Supports:
- Single requests:
{"jsonrpc": "2.0", "method": "...", "params": {...}, "id": 1} - Batch requests: Array of requests, executed concurrently
- Notifications: Requests without
idproduce no response - Standard error codes: -32700 (parse), -32601 (method not found), etc.
- Backward compatible: Legacy
{method, params}format still works
JSON-RPC 2.0 Types
use ultimo::rpc::{JsonRpcRequest, JsonRpcResponse, JsonRpcErrorResponse, JsonRpcError, JsonRpcOutput, error_code};Middleware
Built-in Middleware
logger()
Log all incoming requests with method, path, and response time.
app.use_middleware(ultimo::middleware::builtin::logger());cors()
Enable CORS with permissive defaults (allows all origins, methods, and headers).
app.use_middleware(ultimo::middleware::builtin::cors());powered_by()
Add X-Powered-By: Ultimo header to all responses. Included by default in Ultimo::new().
app.use_middleware(ultimo::middleware::builtin::powered_by());security_headers() / SecurityHeaders
Secure-by-default response headers (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy; CSP opt-in). See Security.
app.use_middleware(ultimo::middleware::builtin::security_headers());
// or customized:
app.use_middleware(
ultimo::middleware::builtin::SecurityHeaders::new()
.csp("default-src 'self'")
.build(),
);IpFilter — IP allow/deny (CIDR)
Filters requests by client IP against an allow-list or deny-list of CIDR networks.
Respects X-Forwarded-For when trust_proxy is enabled.
use ultimo::middleware::builtin::IpFilter;
// Allow only private networks:
app.use_middleware(IpFilter::allow(&[
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.1",
]).build());
// Or deny specific ranges:
app.use_middleware(IpFilter::deny(&["203.0.113.0/24"]).build());rate_limiter() / RateLimiter — Rate limiting (token bucket)
Per-IP, per-header, or global rate limiting. Returns 429 Too Many Requests with
Retry-After header when the limit is exceeded.
use ultimo::middleware::builtin::{rate_limiter, RateLimiter, RateLimitKey};
// Default: 100 requests per 60 seconds, keyed by client IP
app.use_middleware(rate_limiter());
// Custom: 10 requests per second, keyed by API key header
app.use_middleware(
RateLimiter::new(10, 1)
.key(RateLimitKey::Header("X-API-Key".into()))
.build()
);
// Global rate limit (all requests share one bucket)
app.use_middleware(
RateLimiter::new(1000, 60)
.key(RateLimitKey::Global)
.build()
);compression() / Compression (requires compression feature)
Automatic response compression — brotli preferred, gzip fallback. Pure Rust
(no C deps). Skips binary MIME types, already-encoded responses, and bodies
below min_size (default 1 024 bytes). Always sets Vary: Accept-Encoding.
use ultimo::middleware::builtin::{compression, Compression};
// Defaults: brotli + gzip, min_size = 1024
app.use_middleware(compression());
// Customized:
app.use_middleware(
Compression::new()
.gzip()
.brotli()
.min_size(512)
.build(),
);See Compression.
csrf() / Csrf (requires csrf feature)
Double-submit-cookie CSRF protection (constant-time compare; unsafe methods must echo the cookie token in a header). See Security.
app.use_middleware(ultimo::csrf::csrf());
// or: ultimo::csrf::Csrf::new().header_name("x-csrf-token").build()Jwt (requires jwt feature)
HS256 JWT auth — verifies signed bearer/cookie tokens, attaches claims to the
Context, and issues tokens. The algorithm is pinned (alg: none rejected) and
exp is validated by default. Builder: hs256(secret), issuer(s),
audience(s), leeway(secs), from_bearer(), from_cookie(name), optional(),
sign(&claims), build(). See JWT Authentication.
use ultimo::auth::jwt::Jwt;
let jwt = Jwt::hs256(b"super-secret-key");
app.use_middleware(jwt.clone().build());
let token = jwt.sign(&claims)?;ApiKey (requires api-key feature)
API-key auth — validates a presented key against a pluggable ApiKeyStore,
resolving it to an ApiKeyIdentity { id, scopes } attached to the Context;
401 on missing/invalid. The built-in StaticKeys store hashes keys with SHA-256
and compares in constant time. Builder: new(store), header_name(name),
from_query(name), optional(), build(). See API-Key Authentication.
use ultimo::auth::api_key::{ApiKey, StaticKeys};
let store = StaticKeys::new().insert("key-abc", "service-a");
app.use_middleware(ApiKey::new(store).build()); // header "x-api-key"Custom Middleware
Create custom middleware by implementing the middleware function signature:
use ultimo::middleware::Next;
fn my_middleware() -> impl IntoMiddleware {
|ctx: Context, next: Next| async move {
// Before handler
println!("Before: {}", ctx.req.uri());
// Call next middleware/handler
let mut response = next(ctx).await?;
// After handler
response.headers.insert("X-Custom", "value".parse().unwrap());
Ok(response)
}
}
app.use_middleware(my_middleware());OpenAPI
OpenApiSpec
Generate OpenAPI 3.0 specifications for your API.
use ultimo::openapi::OpenApiSpec;
let mut spec = OpenApiSpec::new("My API", "1.0.0");
spec.add_endpoint(
"/users",
"get",
"Get all users",
Some("UserListInput"),
"UserList",
);
let json = spec.to_json()?;Methods
new(title: &str, version: &str) -> Self
Create a new OpenAPI specification.
add_endpoint(&mut self, path: &str, method: &str, summary: &str, request_schema: Option<&str>, response_schema: &str)
Add an API endpoint to the specification.
add_schema(&mut self, name: &str, schema: Value)
Add a JSON schema definition.
to_json(&self) -> Result<String>
Serialize the specification to JSON.
Validation
validate<T: Validate>(value: &T) -> Result<()>
Validate a value using the validator crate rules.
use ultimo::prelude::*;
use validator::Validate;
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 3, max = 50))]
name: String,
#[validate(email)]
email: String,
}
async fn create_user(ctx: Context) -> Result<Response> {
let input: CreateUser = ctx.req.json().await?;
// Validate input
validate(&input)?;
// Process valid data
ctx.json(json!({"success": true})).await
}Validation errors return a 400 Bad Request with detailed error messages.
Error Handling
UltimoError
The main error type for the framework.
pub enum UltimoError {
BadRequest(String),
NotFound(String),
Unauthorized(String),
Forbidden(String),
Internal(String),
ValidationError(String),
DatabaseError(String),
}Usage
// Return errors from handlers
if user.is_none() {
return Err(UltimoError::NotFound("User not found".to_string()));
}
// Or use the ? operator
let id: u32 = ctx.req.param("id")?.parse()?;Errors automatically serialize to JSON:
{
"error": "NotFound",
"message": "User not found"
}Errors that wrap internal machinery (I/O errors, hyper/HTTP errors) return a
generic "internal server error" message to the client instead of the raw
underlying error text — OS error strings can contain filesystem paths and
other details that shouldn't reach callers. The full error is still available
server-side via Display/Debug for logging. Client-input errors (JSON
parse failures, validation errors) are unaffected and still return their
specific message.
Database Integration
SQLx Support (requires sqlx feature)
Ultimo's SQLx integration re-exposes sqlx types (sqlx::Pool, PgPoolOptions, …)
directly through its own public API, so your sqlx dependency version must match
the major version Ultimo was built against (currently 0.8). Bumping Ultimo's
sqlx requirement is treated as a breaking change for exactly this reason.
use sqlx::postgres::PgPoolOptions;
let pool = PgPoolOptions::new()
.max_connections(5)
.connect("postgres://localhost/mydb")
.await?;
app.with_sqlx(pool);Access in handlers:
async fn get_user(ctx: Context) -> Result<Response> {
let db = ctx.db()?;
let pool = db.sqlx_pool::<sqlx::Postgres>()?;
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(1)
.fetch_one(pool)
.await?;
ctx.json(user).await
}Diesel Support (requires diesel feature)
use diesel::r2d2::{self, ConnectionManager};
use diesel::PgConnection;
let manager = ConnectionManager::<PgConnection>::new("postgres://localhost/mydb");
let pool = r2d2::Pool::builder().build(manager)?;
app.with_diesel(pool);Access in handlers:
async fn get_user(ctx: Context) -> Result<Response> {
let db = ctx.db()?;
let pool = db.diesel_pool::<PgConnection>()?;
let conn = pool.get()?;
let user = users::table.find(1).first::<User>(&conn)?;
ctx.json(user).await
}Types & Prelude
Prelude Module
Import everything you need in one line:
use ultimo::prelude::*;
// Includes:
// - Ultimo (app)
// - Context (request context)
// - Result, UltimoError (error types)
// - RpcRegistry, RpcRequest, RpcResponse (RPC system)
// - middleware (middleware module)
// - validate (validation function)
// - serde::{Deserialize, Serialize}
// - serde_json::json
// - validator::ValidateResponse Type
pub struct Response {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Full<Bytes>,
}Responses are typically created through Context methods, but you can construct them manually if needed.
Feature Flags
Enable optional functionality through Cargo features:
[dependencies]
ultimo = { version = "0.6", features = ["sqlx-postgres"] }Available Features
All features are off by default (default = []).
websocket- RFC 6455 WebSocket support (zero extra dependencies)session- Cookie-based session management (Sessions)csrf- Double-submit-cookie CSRF protection (Security)jwt- HS256 JWT authentication middleware (JWT Authentication)api-key- API-key authentication with a pluggable store (API-Key Authentication)static-files- Static file serving + SPA fallback (serve_static,serve_spa) (Static Files)compression- Automatic gzip/brotli response compression (compression(),Compression) (Compression)client-gen- Derive RPC client TypeScript types from Rust types viats-rs;query/mutationinfer types from#[derive(TS)]structs (string-typedquery_with_types/mutation_with_typesremain as escape hatches)testing- Testing utilities:TestClient, assertions, helpers (Testing)test-helpers- WebSocket test helpers (for integration tests)sqlx-postgres/sqlx-mysql/sqlx-sqlite- SQLx integration per backenddiesel-postgres/diesel-mysql/diesel-sqlite- Diesel integration per backend
ultimo = { version = "0.6", features = ["websocket", "session", "sqlx-postgres"] }Next Steps
- Getting Started - Build your first Ultimo app
- RPC System - Type-safe APIs with auto-generated clients
- Middleware - Composable request/response handling
- Database - SQLx and Diesel integration
- Examples - Sample projects