Build your first API in under 5 minutes.
- Rust 1.78 or later (MSRV)
- Cargo (comes with Rust)
# Check your Rust version
rustc --versionAdd RustAPI to your Cargo.toml:
[dependencies]
rustapi-rs = "0.1.335"You can also rename the crate if you prefer shorter macro paths:
[dependencies]
api = { package = "rustapi-rs", version = "0.1.335" }Or with specific features:
[dependencies]
rustapi-rs = { version = "0.1.335", features = ["extras-jwt", "extras-cors", "protocol-toon", "protocol-ws", "protocol-view"] }| Feature | Description |
|---|---|
core |
Default stable core (core-openapi, core-tracing) |
core-openapi |
OpenAPI + docs endpoint support |
core-tracing |
Tracing middleware and instrumentation |
protocol-toon |
LLM-optimized TOON format |
protocol-ws |
WebSocket support |
protocol-view |
Template engine (Tera) |
protocol-grpc |
gRPC bridge helpers |
extras-jwt |
JWT authentication |
extras-cors |
CORS middleware |
extras-rate-limit |
IP-based rate limiting |
extras-config |
Environment/config helpers |
full |
core + protocol-all + extras-all |
Create a new project:
cargo new hello-rustapi
cd hello-rustapiAdd the dependency:
cargo add rustapi-rsEdit src/main.rs:
use rustapi_rs::prelude::*;
#[derive(Serialize, Schema)]
struct Message {
greeting: String,
}
#[rustapi_rs::get("/")]
async fn hello() -> Json<Message> {
Json(Message {
greeting: "Hello, World!".into(),
})
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
RustApi::auto().run("127.0.0.1:8080").await
}Run it:
cargo runTest it:
# Terminal / PowerShell
curl http://localhost:8080/
# Or open in browser
# http://localhost:8080/docs ← Swagger UIuse rustapi_rs::prelude::*;This imports everything you need:
RustApi— Application builderJson,Path,Query,State— ExtractorsSerialize,Deserialize— Serde macrosSchema— OpenAPI schema generationget,post,put,patch,delete— Route functions
#[derive(Serialize, Schema)]
struct Message {
greeting: String,
}Serialize— Enables JSON serializationSchema— Generates OpenAPI documentation automatically
#[rustapi_rs::get("/")]
async fn hello() -> Json<Message> { ... }The #[rustapi_rs::get] macro:
- Registers the route at compile time
- Generates OpenAPI documentation
- Works with
RustApi::auto()for zero-config routing
Route macros also work through dependency aliases, e.g. #[api::get("/")]
when rustapi-rs is renamed to api in Cargo.toml.
RustApi::auto().run("127.0.0.1:8080").awaitRustApi::auto() automatically:
- Discovers all
#[rustapi_rs::get/post/...]routes - Enables Swagger UI at
/docs - Enables OpenAPI spec at
/openapi.json
#[derive(Serialize, Schema)]
struct User {
id: u64,
name: String,
}
#[rustapi_rs::get("/users/{id}")]
async fn get_user(Path(id): Path<u64>) -> Json<User> {
Json(User {
id,
name: format!("User {}", id),
})
}Test: curl http://localhost:8080/users/42
#[derive(Deserialize, Schema)]
struct Pagination {
page: Option<u32>,
limit: Option<u32>,
}
#[rustapi_rs::get("/users")]
async fn list_users(Query(params): Query<Pagination>) -> Json<Vec<User>> {
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(10);
// Fetch users...
Json(vec![])
}Test: curl http://localhost:8080/users?page=2&limit=20
#[derive(Deserialize, Schema)]
struct CreateUser {
name: String,
email: String,
}
#[rustapi_rs::post("/users")]
async fn create_user(Json(body): Json<CreateUser>) -> Json<User> {
Json(User {
id: 1,
name: body.name,
})
}Test:
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'Add validation to your requests:
use rustapi_rs::prelude::*;
#[derive(Deserialize, Validate, Schema)]
struct CreateUser {
#[validate(length(min = 3, max = 50))]
name: String,
#[validate(email)]
email: String,
#[validate(range(min = 0, max = 150))]
age: u8,
}
#[rustapi_rs::post("/users")]
async fn create_user(ValidatedJson(body): ValidatedJson<CreateUser>) -> Json<User> {
// `body` is guaranteed valid at this point
Json(User { id: 1, name: body.name })
}Invalid requests return 422 with detailed errors:
{
"status": 422,
"error_type": "validation_error",
"message": "Request validation failed",
"fields": [
{"field": "email", "message": "Invalid email format"},
{"field": "age", "message": "Must be between 0 and 150"}
]
}| Validator | Usage |
|---|---|
email |
#[validate(email)] |
length |
#[validate(length(min = 1, max = 100))] |
range |
#[validate(range(min = 0, max = 999))] |
regex |
#[validate(regex(path = "PHONE_REGEX"))] |
url |
#[validate(url)] |
custom |
#[validate(custom(function = "my_validator"))] |
Share data across handlers:
use std::sync::Arc;
use tokio::sync::RwLock;
type Db = Arc<RwLock<Vec<User>>>;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let db: Db = Arc::new(RwLock::new(vec![]));
RustApi::new()
.state(db)
.route("/users", get(list_users))
.route("/users", post(create_user))
.run("127.0.0.1:8080")
.await
}
async fn list_users(State(db): State<Db>) -> Json<Vec<User>> {
let users = db.read().await;
Json(users.clone())
}
async fn create_user(
State(db): State<Db>,
Json(body): Json<CreateUser>,
) -> Json<User> {
let mut users = db.write().await;
let user = User { id: users.len() as u64 + 1, name: body.name };
users.push(user.clone());
Json(user)
}RustAPI provides the Result type alias:
use rustapi_rs::prelude::*;
#[rustapi_rs::get("/users/{id}")]
async fn get_user(Path(id): Path<u64>) -> Result<Json<User>> {
if id == 0 {
return Err(ApiError::bad_request("ID cannot be zero"));
}
let user = find_user(id)
.ok_or_else(|| ApiError::not_found(format!("User {} not found", id)))?;
Ok(Json(user))
}ApiError::bad_request("message") // 400
ApiError::unauthorized("message") // 401
ApiError::forbidden("message") // 403
ApiError::not_found("message") // 404
ApiError::conflict("message") // 409
ApiError::unprocessable("message") // 422
ApiError::internal("message") // 500rustapi-rs = { version = "0.1.335", features = ["extras-cors"] }use rustapi_rs::extras::cors::CorsLayer;
RustApi::new()
.layer(CorsLayer::permissive()) // Allow all origins
// Or configure:
// .layer(CorsLayer::new()
// .allow_origin("https://example.com")
// .allow_methods(["GET", "POST"])
// .allow_headers(["Content-Type"]))
.route("/api/data", get(data))
.run("0.0.0.0:8080")
.awaitrustapi-rs = { version = "0.1.335", features = ["extras-jwt"] }use rustapi_rs::extras::jwt::{AuthUser, JwtLayer};
#[derive(Serialize, Deserialize)]
struct Claims {
sub: String,
exp: u64,
}
RustApi::new()
.layer(JwtLayer::new("your-secret-key").skip_paths(["/login", "/health"]))
.route("/protected", get(protected))
.route("/login", post(login))
.run("0.0.0.0:8080")
.await
async fn protected(user: AuthUser<Claims>) -> Json<Response> {
Json(Response {
message: format!("Hello, {}", user.0.sub)
})
}rustapi-rs = { version = "0.1.335", features = ["extras-rate-limit"] }use rustapi_rs::extras::rate_limit::RateLimitLayer;
RustApi::new()
.layer(RateLimitLayer::new(100, Duration::from_secs(60))) // 100 req/min
.route("/api", get(handler))
.run("0.0.0.0:8080")
.awaitrustapi-rs = { version = "0.1.335", features = ["protocol-toon"] }use rustapi_rs::protocol::toon::{Toon, LlmResponse, AcceptHeader};
// Direct TOON response
#[rustapi_rs::get("/ai/users")]
async fn ai_users() -> Toon<UsersResponse> {
Toon(get_users())
}
// Content negotiation based on Accept header
#[rustapi_rs::get("/users")]
async fn users(accept: AcceptHeader) -> LlmResponse<UsersResponse> {
LlmResponse::new(get_users(), accept.preferred)
}Response includes token counting headers:
X-Token-Count-JSON: Token count for JSON formatX-Token-Count-TOON: Token count for TOON formatX-Token-Savings: Percentage saved (e.g., "57.8%")
Real-time bidirectional communication:
rustapi-rs = { version = "0.1.335", features = ["protocol-ws"] }use rustapi_rs::protocol::ws::{WebSocket, WebSocketUpgrade, WebSocketStream, Message};
#[rustapi_rs::get("/ws")]
async fn websocket(ws: WebSocket) -> WebSocketUpgrade {
ws.on_upgrade(handle_connection)
}
async fn handle_connection(mut stream: WebSocketStream) {
while let Some(msg) = stream.recv().await {
match msg {
Message::Text(text) => {
// Echo the message back
stream.send(Message::Text(format!("Echo: {}", text))).await.ok();
}
Message::Close(_) => break,
_ => {}
}
}
}Test with websocat:
websocat ws://localhost:8080/wsServer-side HTML rendering with Tera:
rustapi-rs = { version = "0.1.335", features = ["protocol-view"] }Create a template file templates/index.html:
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
<h1>Hello, {{ name }}!</h1>
</body>
</html>Use in your handler:
use rustapi_rs::protocol::view::{Templates, View, TemplatesConfig};
#[tokio::main]
async fn main() {
let templates = Templates::new(TemplatesConfig {
directory: "templates".into(),
extension: "html".into(),
}).unwrap();
RustApi::new()
.state(templates)
.route("/", get(home))
.run("0.0.0.0:8080")
.await
}
#[derive(Serialize)]
struct HomeData {
title: String,
name: String,
}
#[rustapi_rs::get("/")]
async fn home(templates: Templates) -> View<HomeData> {
View::new(&templates, "index.html", HomeData {
title: "Welcome".into(),
name: "World".into(),
})
}Scaffold new RustAPI projects quickly:
# Install the CLI
cargo install cargo-rustapi
# Create a new project
cargo rustapi new my-api
# Interactive mode with template selection
cargo rustapi new my-api --interactive
# Run with hot-reload (auto-restart on file changes)
cargo rustapi watch
# Add features or dependencies
cargo rustapi add extras-cors extras-jwt
# Check environment health
cargo rustapi doctorAvailable templates:
minimal— Basic RustAPI setupapi— REST API with CRUD operationsweb— Full web app with templates and WebSocketfull— Everything included
use rustapi_rs::test::TestClient;
#[tokio::test]
async fn test_hello() {
let app = RustApi::new()
.route("/", get(hello));
let client = TestClient::new(app);
let res = client.get("/").send().await;
assert_eq!(res.status(), 200);
let body: Message = res.json().await;
assert_eq!(body.greeting, "Hello, World!");
}
#[tokio::test]
async fn test_create_user() {
let app = RustApi::new()
.route("/users", post(create_user));
let client = TestClient::new(app);
let res = client
.post("/users")
.json(&CreateUser { name: "Alice".into(), email: "alice@test.com".into() })
.send()
.await;
assert_eq!(res.status(), 200);
}- 📖 Philosophy — Understand our design principles
- 🏗️ Architecture — Deep dive into internals
- 📚 Features — Complete feature documentation
- 💡 Examples — Real-world examples
RustAPI now ships standard health probes out of the box:
use rustapi_rs::prelude::*;
#[rustapi_rs::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
RustApi::auto()
.health_endpoints()
.run("127.0.0.1:8080")
.await
}This enables:
/health— aggregate health report/ready— readiness probe for orchestrators/live— lightweight liveness probe
If you want a stronger production-ready baseline from day one, use:
use rustapi_rs::prelude::*;
#[rustapi_rs::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
RustApi::auto()
.production_defaults("hello-api")
.run("127.0.0.1:8080")
.await
}This installs request IDs, tracing middleware, and probe endpoints together.
If you want a custom probe, you can still define one manually:
#[rustapi_rs::get("/health")]
async fn health() -> &'static str {
"OK"
}#[rustapi_rs::get("/data")]
async fn data() -> Result<Json<Data>> {
match fetch_data().await {
Ok(data) => Ok(Json(data)),
Err(_) => Err(ApiError::not_found("Data not found")),
}
}use rustapi_rs::response::Created;
#[rustapi_rs::post("/users")]
async fn create_user(Json(body): Json<CreateUser>) -> Created<User> {
let user = User { id: 1, name: body.name };
Created(user) // Returns 201 Created
}Make sure you're using RustApi::auto():
// ✅ Correct
RustApi::auto().run("0.0.0.0:8080").await
// ❌ Won't find macro routes
RustApi::new().run("0.0.0.0:8080").awaitEnsure your types implement required traits:
// ✅ For request bodies
#[derive(Deserialize)]
struct RequestBody { ... }
// ✅ For responses
#[derive(Serialize)]
struct ResponseBody { ... }
// ✅ For OpenAPI docs
#[derive(Schema)]
struct AnyBody { ... }Check that core-openapi is enabled (it is included in the default core feature):
rustapi-rs = { version = "0.1.335", features = ["core-openapi"] }Use the new doctor command to diagnose:
cargo rustapi doctorHappy coding! 🦀