| Crates.io | ultimo |
| lib.rs | ultimo |
| version | 0.2.1 |
| created_at | 2025-11-22 11:24:38.033681+00 |
| updated_at | 2026-01-04 13:18:32.746292+00 |
| description | Modern Rust web framework with automatic TypeScript client generation |
| homepage | https://ultimo.dev |
| repository | https://github.com/ultimo-rs/ultimo |
| max_upload_size | |
| id | 1945232 |
| size | 497,623 |
Type-safe web framework with automatic TypeScript client generation
Website • Documentation • Getting Started • Examples
⚡ Industry-leading performance (158k+ req/sec, 0.6ms latency) - Lightweight, type-safe Rust web framework with automatic TypeScript client generation for type-safe full-stack development.
ultimo CLISee the full roadmap for upcoming features:
Ultimo delivers exceptional performance, matching industry-leading frameworks:
| Framework | Throughput | Avg Latency | vs Python |
|---|---|---|---|
| Ultimo | 158k req/sec | 0.6ms | 15x faster |
| Axum (Rust) | 153k req/sec | 0.6ms | 15x faster |
| Hono (Bun) | 132k req/sec | 0.8ms | 13x faster |
| Hono (Node) | 62k req/sec | 1.6ms | 6x faster |
| FastAPI | 10k req/sec | 9.5ms | baseline |
Zero performance penalty for automatic RPC generation, OpenAPI docs, and client SDK generation.
📖 Complete documentation available at docs.ultimo.dev →
Comprehensive guides covering:
- Getting Started & Installation
- Routing & Middleware
- RPC System & TypeScript Clients
- OpenAPI Support
- Database Integration (SQLx/Diesel)
- Testing Patterns
- CLI Tools
- And more...
Create a basic REST API with routes and JSON responses:
use ultimo::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUserInput {
name: String,
email: String,
}
#[tokio::main]
async fn main() -> ultimo::Result<()> {
let mut app = Ultimo::new();
// GET /users - List all users
app.get("/users", |ctx: Context| async move {
let users = vec![
User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() },
User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() },
];
ctx.json(users).await
});
// GET /users/:id - Get user by ID
app.get("/users/:id", |ctx: Context| async move {
let id: u32 = ctx.param("id")?.parse()?;
let user = User {
id,
name: format!("User {}", id),
email: format!("user{}@example.com", id),
};
ctx.json(user).await
});
// POST /users - Create new user
app.post("/users", |ctx: Context| async move {
let input: CreateUserInput = ctx.req.json().await?;
let user = User {
id: 3,
name: input.name,
email: input.email,
};
ctx.json(user).await
});
println!("🚀 Server running on http://127.0.0.1:3000");
app.listen("127.0.0.1:3000").await
}
Test it:
# List users
curl http://localhost:3000/users
# Get specific user
curl http://localhost:3000/users/1
# Create user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Charlie","email":"charlie@example.com"}'
[dependencies]
ultimo = { path = "./ultimo" }
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
Ultimo supports two RPC modes:
use ultimo::prelude::*;
use ultimo::rpc::RpcMode;
#[tokio::main]
async fn main() -> Result<()> {
let mut app = Ultimo::new();
// Create RPC registry in REST mode
let rpc = RpcRegistry::new_with_mode(RpcMode::Rest);
// Register query (will use GET)
rpc.query(
"getUser",
|input: GetUserInput| async move {
Ok(User {
id: input.id,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
})
},
"{ id: number }".to_string(),
"User".to_string(),
);
// Register mutation (will use POST)
rpc.mutation(
"createUser",
|input: CreateUserInput| async move {
Ok(User { /* ... */ })
},
"{ name: string; email: string }".to_string(),
"User".to_string(),
);
// Generate TypeScript client with REST endpoints
rpc.generate_client_file("../frontend/src/lib/ultimo-client.ts")?;
// Mount individual endpoints
app.get("/api/getUser", /* handler */);
app.post("/api/createUser", /* handler */);
app.listen("127.0.0.1:3000").await
}
use ultimo::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
let mut app = Ultimo::new();
// Create RPC registry (JSON-RPC is default)
let rpc = RpcRegistry::new();
// Register procedures
rpc.register_with_types(
"getUser",
|input: GetUserInput| async move {
Ok(User {
id: input.id,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
})
},
"{ id: number }".to_string(),
"User".to_string(),
);
// Generate TypeScript client
rpc.generate_client_file("../frontend/src/lib/ultimo-client.ts")?;
// Single RPC endpoint
app.post("/rpc", move |ctx: Context| {
let rpc = rpc.clone();
async move {
let req: RpcRequest = ctx.req.json().await?;
let result = rpc.call(&req.method, req.params).await?;
ctx.json(result).await
}
});
app.listen("127.0.0.1:3000").await
}
When to use each mode:
The generated client works the same way regardless of RPC mode:
import { UltimoRpcClient } from "./lib/ultimo-client";
// REST mode: client uses GET/POST to /api/getUser, /api/createUser
// JSON-RPC mode: client uses POST to /rpc with method dispatch
const client = new UltimoRpcClient(); // Uses appropriate base URL
// Same API for both modes - fully type-safe!
const user = await client.getUser({ id: 1 });
console.log(user.name); // ✅ TypeScript autocomplete works!
const newUser = await client.createUser({
name: "Bob",
email: "bob@example.com",
});
Automatically generate OpenAPI 3.0 specs from your RPC procedures:
use ultimo::prelude::*;
use ultimo::rpc::RpcMode;
let rpc = RpcRegistry::new_with_mode(RpcMode::Rest);
// Register procedures
rpc.query("getUser", handler, "{ id: number }", "User");
rpc.mutation("createUser", handler, "{ name: string }", "User");
// Generate OpenAPI spec
let openapi = rpc.generate_openapi("My API", "1.0.0", "/api");
openapi.write_to_file("openapi.json")?;
Use with external tools:
# View in Swagger UI
docker run -p 8080:8080 -e SWAGGER_JSON=/openapi.json \
-v $(pwd)/openapi.json:/openapi.json swaggerapi/swagger-ui
# Run Prism mock server
npx @stoplight/prism-cli mock openapi.json
# Generate clients in any language
npx @openapitools/openapi-generator-cli generate \
-i openapi.json -g typescript-fetch -o ./client
Install the Ultimo CLI:
cargo install --path ultimo-cli
# Generate client from your Rust backend
ultimo generate --project ./backend --output ./frontend/src/lib/client.ts
# Short form
ultimo generate -p ./backend -o ./frontend/src/client.ts
ultimo new my-app --template fullstack # Create new project
ultimo dev --port 3000 # Development server with hot reload
ultimo build --profile release # Production build
Run the included demo script to see everything in action:
./demo.sh
This will:
# Install CLI
./install-cli.sh
# Or build manually
cargo build --release --manifest-path ultimo-cli/Cargo.toml
# Verify installation
ultimo --help
# Backend with auto-generation
cd examples/react-backend
cargo run --release
# Frontend
cd examples/react-app
npm install
npm run dev
# Generate client manually
ultimo generate -p ./examples/react-backend -o /tmp/client.ts
use ultimo::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
let mut app = Ultimo::new();
let rpc = RpcRegistry::new();
// 1. Register RPC procedures with TypeScript types
rpc.register_with_types(
"listUsers",
|_params: ()| async move {
Ok(json!({"users": [...], "total": 2}))
},
"{}".to_string(),
"{ users: User[]; total: number }".to_string(),
);
// 2. Auto-generate TypeScript client on startup
rpc.generate_client_file("../frontend/src/lib/ultimo-client.ts")?;
println!("✅ TypeScript client generated");
// 3. Add RPC endpoint
app.post("/rpc", move |mut c: Context| {
let rpc = rpc.clone();
async move {
let req: RpcRequest = c.req.json().await?;
let result = rpc.call(&req.method, req.params).await?;
c.json(RpcResponse { result })
}
});
app.listen("127.0.0.1:3000").await
}
// src/lib/ultimo-client.ts - Auto-generated, don't edit!
export class UltimoRpcClient {
async listUsers(params: {}): Promise<{ users: User[]; total: number }> {
return this.call("listUsers", params);
}
}
// src/App.tsx - Use the generated client
import { UltimoRpcClient } from "./lib/ultimo-client";
import { useQuery } from "@tanstack/react-query";
const client = new UltimoRpcClient("/api/rpc");
function UserList() {
const { data } = useQuery({
queryKey: ["users"],
queryFn: () => client.listUsers({}), // ✅ Fully type-safe!
});
return (
<div>
{data?.users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
# Terminal 1: Start backend (auto-generates client)
cd backend
cargo run --release
# ✅ TypeScript client generated
# Terminal 2: Start frontend
cd frontend
npm run dev
# Terminal 3: Regenerate client manually if needed
ultimo generate -p ./backend -o ./frontend/src/lib/client.ts
✅ Single Source of Truth - Types defined in Rust, automatically propagate to TypeScript
✅ No Manual Typing - TypeScript types generated automatically from Rust
✅ Type Safety - Catch API mismatches at compile time
✅ Great DX - Full IDE autocomplete and type checking
✅ Zero Maintenance - Client updates automatically when backend changes
ultimo-rs/
├── Cargo.toml (workspace)
├── ultimo/
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs (public API)
│ ├── app.rs (main application)
│ ├── context.rs (request/response context)
│ ├── error.rs (error handling)
│ ├── response.rs (response builder)
│ ├── router.rs (routing engine)
│ ├── handler.rs (handler traits)
│ ├── middleware.rs (middleware system)
│ ├── validation.rs (validation helpers)
│ ├── upload.rs (file upload handling)
│ ├── guard.rs (authentication guards)
│ └── rpc.rs (RPC system)
└── examples/
└── basic/
└── src/main.rs
Create a comprehensive error system that returns structured JSON responses with proper HTTP status codes. Support validation errors with field-level details, authentication errors, authorization errors, and general HTTP errors.
Provide response methods directly on Context (like Hono's c.json(), c.text(), c.html()). Support for JSON, text, HTML, redirects, custom headers, and status codes. Methods should be chainable where appropriate.
Wrap incoming HTTP requests with a context object that provides:
c.req.param() for path parametersc.req.query() for query parametersc.req.json(), c.req.text(), c.req.parse_body() for request bodiesc.req.header() for headersc.set() / c.get() for passing values between middlewarec.json(), c.text(), c.html(), c.redirect()c.status() and c.header() for setting response metadataImplement efficient path-based routing with support for:
/users)/users/:id)/users/:userId/posts/:postId)Create a trait-based system for request handlers that supports async functions. Handlers receive a Context and return a Result
Implement composable middleware that can:
next() await pointc.set() / c.get()next()await next() then modify c.resIntegrate with the validator crate to provide automatic request body validation with custom error messages. Convert validation failures into structured JSON error responses.
Support multipart form data parsing with:
Create a guard system for protecting routes with:
Implement rate limiting middleware with:
Build a type-safe RPC system where:
/rpc/{procedure-name} (POST method)./bindings/ directory/rpc/{namespace}.{procedure}Tie everything together in an Ultimo struct that:
use ultimo::prelude::*;
// GET request with JSON response
app.get("/users", |c| async move {
c.json(json!({"users": ["Alice", "Bob"]}))
});
// Path parameters
app.get("/users/:id", |c| async move {
let id = c.req.param("id")?;
c.json(json!({"id": id}))
});
// Query parameters
app.get("/search", |c| async move {
let q = c.req.query("q")?;
c.json(json!({"query": q}))
});
#[derive(Deserialize, Validate, TS)]
#[ts(export)]
struct CreateUser {
#[validate(length(min = 3, max = 50))]
name: String,
#[validate(email)]
email: String,
}
app.post("/users", |mut c| async move {
// Parse and validate in one step
let input: CreateUser = c.req.json().await?;
validate(&input)?;
// Use the validated data
let user = create_user(input);
c.status(201);
c.json(user)
});
use ultimo::middleware::{logger, cors};
// Global middleware
app.use_middleware(logger());
app.use_middleware(cors::new()
.allow_origin("https://example.com")
.allow_methods(vec!["GET", "POST"]));
// Custom middleware
app.use_middleware(|c, next| async move {
c.set("request_id", generate_id());
let start = Instant::now();
next().await?;
let duration = start.elapsed();
c.res.headers.append("X-Response-Time", duration.as_millis().to_string());
Ok(())
});
use ultimo::guards::{bearer_auth, api_key_auth};
// Protect specific routes
let auth = bearer_auth(vec!["secret_token_123"]);
app.get("/protected", auth, |c| async move {
c.json(json!({"message": "You are authenticated!"}))
});
// Or use as middleware for a group
app.use_middleware(api_key_auth(vec!["api_key_123"]));
app.post("/upload", |mut c| async move {
let form_data = c.req.parse_body().await?;
for (field_name, file) in form_data.files {
if file.is_image() {
let path = format!("./uploads/{}", file.name);
file.save(&path).await?;
}
}
c.json(json!({"uploaded": form_data.files.len()}))
});
#[derive(Deserialize, TS)]
#[ts(export)]
struct CalculateInput {
a: i32,
b: i32,
}
#[derive(Serialize, TS)]
#[ts(export)]
struct CalculateOutput {
result: i32,
}
app.rpc("calculate", |input: CalculateInput| async move {
Ok(CalculateOutput {
result: input.a + input.b,
})
});
// Access at: POST /rpc/calculate
// TypeScript types auto-generated in ./bindings/
// Set data in middleware
app.use_middleware(|c, next| async move {
let user = authenticate(&c).await?;
c.set("user", user);
next().await
});
// Access in handler
app.get("/profile", |c| async move {
let user: User = c.get("user")?;
c.json(user)
});
All errors return JSON with structure:
{
"error": "Error Type",
"message": "Human readable message",
"details": [] // Optional, for validation errors
}
/users/:id matches /users/123 → {id: "123"}/users/:userId/posts/:postId matches /users/1/posts/2 → {userId: "1", postId: "2"}Middleware executes in order, each can call next() to continue the chain or return early to short-circuit.
Track requests per client within a time window. Reject requests exceeding the limit with 429 (Too Many Requests) status code.
Types decorated with #[ts(export)] automatically generate .ts files in the ./bindings/ directory for frontend use.
When complete, this code should work:
#[tokio::main]
async fn main() -> ultimo::Result<()> {
let mut app = Ultimo::new();
app.use_middleware(ultimo::middleware::logger());
app.get("/", |ctx| async move {
Ok(Context::json(json!({"message": "Hello Ultimo!"}))?)
});
app.post("/users", |mut ctx| async move {
let input: CreateUser = ctx.req.json().await?;
validate(&input)?;
Ok(Context::json(create_user(input))?)
});
app.listen("127.0.0.1:3000").await
}
And support curl commands like:
curl http://localhost:3000/
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"Alice"}'
curl http://localhost:3000/protected -H "Authorization: Bearer token123"
Focus on clean abstractions and excellent developer experience. The framework should feel natural to Rust developers while being immediately familiar to those coming from Hono.js or Express.
This project uses Moonrepo for monorepo management. See MOONREPO.md for detailed commands.
Quick Start:
# Install Moonrepo
curl -fsSL https://moonrepo.dev/install/moon.sh | bash
# Install git hooks (recommended)
./scripts/install-hooks.sh
# Build core framework
moon run ultimo:build
# Run all tests
moon run :test
# Check code quality
moon run ultimo:clippy
# Build documentation
moon run docs-site:build
We provide pre-commit and pre-push hooks to ensure code quality:
# Install hooks
./scripts/install-hooks.sh
What the hooks do:
cargo fmtThe Ultimo framework maintains high test coverage standards with a custom coverage tool built for security and transparency.
We built ultimo-coverage instead of using external tools because:
# Run tests with coverage report
cargo coverage
# Or use make
make coverage
# View HTML report (modern UI with Tailwind CSS)
open target/coverage/html/index.html
Overall: 63.58% ✅ (exceeds 60% minimum threshold)
| Module | Coverage | Status |
|---|---|---|
| database/error | 100% | ✅ Excellent |
| validation | 95.12% | ✅ Excellent |
| response | 92.35% | ✅ Excellent |
| rpc | 85.07% | ✅ Excellent |
| router | 82.41% | ✅ Excellent |
| openapi | 76.21% | ✅ Good |
| context | 40.18% | ⚠️ Improved |
| app | 25.62% | ⚠️ Needs work |
Test Stats:
ultimo-coverage is our custom LLVM-based coverage tool:
# How it works
cd coverage-tool
cargo build --release
# The tool:
# 1. Instruments code with LLVM coverage
# 2. Runs tests and collects profiling data
# 3. Merges .profraw files with llvm-profdata
# 4. Generates reports with llvm-cov
# 5. Filters out dependency code (.cargo/registry, .rustup)
Why it's trustworthy:
Key Features:
Run tests before submitting PRs:
# Run all tests
cargo test --lib
# Check coverage
cargo coverage
# Ensure coverage meets standards
# Overall must be ≥60%, new code should increase coverage
Project Structure:
ultimo/ - Core frameworkultimo-cli/ - CLI tool for project scaffolding and TypeScript generationexamples/ - Example projects demonstrating featuresdocs-site/ - Documentation website (Vocs)scripts/ - Development and testing scripts