Complete reference for all RustAPI features and capabilities.
- Extractors
- Response Types
- Validation
- OpenAPI & Swagger
- Middleware
- TOON Format
- WebSocket
- Template Engine
- Testing
- Error Handling
- Configuration
Extractors parse incoming requests into typed Rust values.
Extract JSON body.
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[rustapi_rs::post("/users")]
async fn create_user(Json(body): Json<CreateUser>) -> Json<User> {
// body is parsed and typed
}Errors:
- 400 Bad Request — Invalid JSON syntax
- 415 Unsupported Media Type — Missing
Content-Type: application/json - 422 Unprocessable Entity — JSON doesn't match schema
Extract URL path parameters.
// Single parameter
#[rustapi_rs::get("/users/{id}")]
async fn get_user(Path(id): Path<u64>) -> Json<User> { ... }
// Multiple parameters
#[derive(Deserialize)]
struct PathParams {
org: String,
repo: String,
}
#[rustapi_rs::get("/orgs/{org}/repos/{repo}")]
async fn get_repo(Path(params): Path<PathParams>) -> Json<Repo> { ... }
// Tuple extraction
#[rustapi_rs::get("/users/{user_id}/posts/{post_id}")]
async fn get_post(Path((user_id, post_id)): Path<(u64, u64)>) -> Json<Post> { ... }Extract query string parameters.
#[derive(Deserialize)]
struct Filters {
page: Option<u32>,
limit: Option<u32>,
search: Option<String>,
}
#[rustapi_rs::get("/users")]
async fn list_users(Query(filters): Query<Filters>) -> Json<Vec<User>> {
let page = filters.page.unwrap_or(1);
let limit = filters.limit.unwrap_or(10);
// ...
}URL: /users?page=2&limit=20&search=alice
Extract application state.
use std::sync::Arc;
use tokio::sync::RwLock;
type AppState = Arc<RwLock<Vec<User>>>;
#[tokio::main]
async fn main() {
let state: AppState = Arc::new(RwLock::new(vec![]));
RustApi::new()
.state(state)
.route("/users", get(list_users))
.run("0.0.0.0:8080")
.await
}
async fn list_users(State(db): State<AppState>) -> Json<Vec<User>> {
let users = db.read().await;
Json(users.clone())
}Extract raw request body as bytes.
use bytes::Bytes;
#[rustapi_rs::post("/upload")]
async fn upload(body: Body) -> &'static str {
let bytes: Bytes = body.into_bytes().await;
// Process raw bytes
"Uploaded"
}Extract request headers.
use rustapi_rs::extract::Headers;
#[rustapi_rs::get("/info")]
async fn info(headers: Headers) -> Json<Info> {
let user_agent = headers.get("user-agent").unwrap_or("unknown");
let auth = headers.get("authorization");
// ...
}Extract cookies (requires cookies feature).
use rustapi_rs::extract::Cookies;
#[rustapi_rs::get("/session")]
async fn session(cookies: Cookies) -> Json<Session> {
let session_id = cookies.get("session_id");
// ...
}Extract client IP address.
use rustapi_rs::extract::ClientIp;
#[rustapi_rs::get("/whoami")]
async fn whoami(ClientIp(ip): ClientIp) -> String {
format!("Your IP: {}", ip)
}Checks X-Forwarded-For, X-Real-IP, then socket address.
Extract authenticated user claims (requires extras-jwt feature).
use rustapi_rs::extract::AuthUser;
#[derive(Deserialize)]
struct Claims {
sub: String,
role: String,
exp: u64,
}
#[rustapi_rs::get("/profile")]
async fn profile(user: AuthUser<Claims>) -> Json<Profile> {
// user.claims contains decoded JWT claims
Json(Profile {
user_id: user.claims.sub,
role: user.claims.role,
})
}Extract and validate JSON body.
use rustapi_rs::extract::ValidatedJson;
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 3))]
name: String,
#[validate(email)]
email: String,
}
#[rustapi_rs::post("/users")]
async fn create_user(ValidatedJson(body): ValidatedJson<CreateUser>) -> Json<User> {
// body is guaranteed to pass validation
}Standard JSON response (200 OK).
#[derive(Serialize)]
struct User { id: u64, name: String }
async fn get_user() -> Json<User> {
Json(User { id: 1, name: "Alice".into() })
}201 Created with JSON body.
use rustapi_rs::response::Created;
async fn create_user(Json(body): Json<CreateUser>) -> Created<User> {
let user = User { id: 1, name: body.name };
Created(user)
}204 No Content.
use rustapi_rs::response::NoContent;
async fn delete_user(Path(id): Path<u64>) -> NoContent {
// delete user...
NoContent
}HTML response.
use rustapi_rs::response::Html;
async fn page() -> Html<String> {
Html("<h1>Hello, World!</h1>".into())
}HTTP redirect.
use rustapi_rs::response::Redirect;
async fn old_route() -> Redirect {
Redirect::permanent("/new-route")
}
async fn temp_redirect() -> Redirect {
Redirect::temporary("/maintenance")
}async fn health() -> &'static str {
"OK"
}
async fn dynamic_text() -> String {
format!("Server time: {}", chrono::Utc::now())
}use rustapi_rs::http::StatusCode;
async fn custom_response() -> (StatusCode, Json<Message>) {
(StatusCode::CREATED, Json(Message { text: "Created!".into() }))
}
async fn with_headers() -> (StatusCode, [(&'static str, &'static str); 1], String) {
(StatusCode::OK, [("X-Custom", "value")], "Hello".into())
}async fn fallible() -> Result<Json<User>> {
let user = find_user(1).ok_or(ApiError::not_found("User not found"))?;
Ok(Json(user))
}Built-in validation using the Validate derive macro.
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 1, max = 100))]
name: String,
#[validate(email)]
email: String,
#[validate(range(min = 0, max = 150))]
age: u8,
}| Validator | Example | Description |
|---|---|---|
email |
#[validate(email)] |
Valid email format |
url |
#[validate(url)] |
Valid URL |
length |
#[validate(length(min = 1, max = 100))] |
String/array length |
range |
#[validate(range(min = 0, max = 999))] |
Numeric range |
regex |
#[validate(regex(path = "PHONE_RE"))] |
Regex pattern |
contains |
#[validate(contains = "@")] |
Contains substring |
must_match |
#[validate(must_match = "password")] |
Fields must match |
custom |
#[validate(custom(function = "fn"))] |
Custom validator |
fn validate_username(username: &str) -> Result<(), validator::ValidationError> {
if username.starts_with("admin") {
return Err(validator::ValidationError::new("reserved_username"));
}
Ok(())
}
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(custom(function = "validate_username"))]
username: String,
}#[derive(Deserialize, Validate)]
struct Address {
#[validate(length(min = 1))]
street: String,
#[validate(length(min = 1))]
city: String,
}
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 1))]
name: String,
#[validate] // Validates nested struct
address: Address,
}Validation failures return 422 Unprocessable Entity:
{
"status": 422,
"error_type": "validation_error",
"message": "Request validation failed",
"error_id": "err_abc123",
"fields": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Must be between 0 and 150"
}
]
}Automatic API documentation generation.
#[derive(Serialize, Schema)]
struct User {
/// The user's unique identifier
id: u64,
/// The user's display name
name: String,
#[schema(format = "email")]
email: String,
#[schema(example = "2024-01-01T00:00:00Z")]
created_at: String,
}#[rustapi_rs::get("/users/{id}")]
#[doc = "Get a user by ID"]
async fn get_user(
/// The user ID to fetch
Path(id): Path<u64>,
) -> Json<User> {
// ...
}- Swagger UI:
http://localhost:8080/docs - OpenAPI JSON:
http://localhost:8080/openapi.json
RustApi::new()
.openapi_info(|info| {
info.title("My API")
.version("1.0.0")
.description("My awesome API")
})
.run("0.0.0.0:8080")
.awaitCross-Origin Resource Sharing.
use rustapi_rs::middleware::CorsLayer;
// Allow all origins
RustApi::new()
.layer(CorsLayer::permissive())
.run("0.0.0.0:8080")
.await
// Custom configuration
RustApi::new()
.layer(
CorsLayer::new()
.allow_origin("https://example.com")
.allow_origin("https://app.example.com")
.allow_methods(["GET", "POST", "PUT", "DELETE"])
.allow_headers(["Content-Type", "Authorization"])
.allow_credentials(true)
.max_age(Duration::from_secs(3600))
)
.run("0.0.0.0:8080")
.awaitJWT authentication.
use rustapi_rs::middleware::JwtLayer;
RustApi::new()
.layer(
JwtLayer::new("your-secret-key")
.skip_paths(["/login", "/register", "/health"])
.algorithm(Algorithm::HS256) // Default
)
.run("0.0.0.0:8080")
.awaitIP-based rate limiting.
use rustapi_rs::middleware::RateLimitLayer;
use std::time::Duration;
RustApi::new()
.layer(RateLimitLayer::new(100, Duration::from_secs(60))) // 100 req/min
.run("0.0.0.0:8080")
.awaitLimit request body size.
use rustapi_rs::middleware::BodyLimitLayer;
RustApi::new()
.layer(BodyLimitLayer::new(1024 * 1024)) // 1 MB
.run("0.0.0.0:8080")
.awaitAdd unique request IDs.
use rustapi_rs::middleware::RequestIdLayer;
RustApi::new()
.layer(RequestIdLayer::new())
.run("0.0.0.0:8080")
.await
// Adds X-Request-ID header to responsesRequest/response logging.
use rustapi_rs::middleware::TracingLayer;
RustApi::new()
.layer(TracingLayer::new())
.run("0.0.0.0:8080")
.await
// Logs: method, path, status, durationPrometheus metrics.
use rustapi_rs::middleware::MetricsLayer;
RustApi::new()
.layer(MetricsLayer::new())
.route("/metrics", get(metrics_handler))
.run("0.0.0.0:8080")
.await
// Metrics:
// - http_requests_total{method, path, status}
// - http_request_duration_seconds{method, path}Middleware executes in order added (first added = outermost):
RustApi::new()
.layer(RequestIdLayer::new()) // 1st - Adds request ID
.layer(TracingLayer::new()) // 2nd - Logs request
.layer(CorsLayer::permissive()) // 3rd - Handles CORS
.layer(RateLimitLayer::new(...))// 4th - Rate limiting
.layer(JwtLayer::new(...)) // 5th - AuthenticationToken-Oriented Object Notation for LLM optimization.
use rustapi_rs::protocol::toon::Toon;
#[rustapi_rs::get("/ai/users")]
async fn ai_users() -> Toon<UsersResponse> {
Toon(get_users())
}use rustapi_rs::protocol::toon::{LlmResponse, AcceptHeader};
#[rustapi_rs::get("/users")]
async fn users(accept: AcceptHeader) -> LlmResponse<UsersResponse> {
// Automatically chooses JSON or TOON based on Accept header
LlmResponse::new(get_users(), accept.preferred)
}LlmResponse adds these headers:
| Header | Description |
|---|---|
X-Token-Count-JSON |
Approximate tokens if JSON |
X-Token-Count-TOON |
Approximate tokens if TOON |
X-Token-Savings |
Percentage saved (e.g., "57.8%") |
X-Format-Used |
"json" or "toon" |
| Accept Header | Result |
|---|---|
application/json |
JSON response |
application/toon |
TOON response |
*/* |
Default (JSON) |
application/toon, application/json |
TOON (preferred) |
JSON:
{"users":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}],"total":2,"page":1}TOON:
users[(id:1,name:Alice,email:alice@example.com)(id:2,name:Bob,email:bob@example.com)]total:2,page:1
Savings: ~50-58% fewer tokens
Real-time bidirectional communication support (requires protocol-ws feature).
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::Binary(data) => {
// Handle binary data
stream.send(Message::Binary(data)).await.ok();
}
Message::Ping(data) => {
stream.send(Message::Pong(data)).await.ok();
}
Message::Close(_) => break,
_ => {}
}
}
}| Type | Description |
|---|---|
Message::Text(String) |
UTF-8 text message |
Message::Binary(Vec<u8>) |
Binary data |
Message::Ping(Vec<u8>) |
Ping frame (keepalive) |
Message::Pong(Vec<u8>) |
Pong response |
Message::Close(Option<CloseFrame>) |
Connection close |
For pub/sub patterns (chat rooms, live updates):
use rustapi_rs::protocol::ws::{Broadcast, Message};
use std::sync::Arc;
#[tokio::main]
async fn main() {
let broadcast = Arc::new(Broadcast::new());
RustApi::new()
.state(broadcast)
.route("/ws", get(websocket))
.route("/broadcast", post(send_broadcast))
.run("0.0.0.0:8080")
.await
}
#[rustapi_rs::get("/ws")]
async fn websocket(
ws: WebSocket,
State(broadcast): State<Arc<Broadcast>>,
) -> WebSocketUpgrade {
let mut rx = broadcast.subscribe();
ws.on_upgrade(move |mut stream| async move {
loop {
tokio::select! {
// Receive from client
msg = stream.recv() => {
match msg {
Some(Message::Close(_)) | None => break,
_ => {}
}
}
// Receive broadcasts
Ok(msg) = rx.recv() => {
if stream.send(msg).await.is_err() {
break;
}
}
}
}
})
}
#[rustapi_rs::post("/broadcast")]
async fn send_broadcast(
State(broadcast): State<Arc<Broadcast>>,
body: String,
) -> &'static str {
broadcast.send(Message::Text(body));
"Sent"
}use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct ConnectionCounter(AtomicUsize);
#[rustapi_rs::get("/ws")]
async fn websocket(
ws: WebSocket,
State(counter): State<Arc<ConnectionCounter>>,
) -> WebSocketUpgrade {
ws.on_upgrade(move |stream| async move {
counter.0.fetch_add(1, Ordering::SeqCst);
handle_connection(stream).await;
counter.0.fetch_sub(1, Ordering::SeqCst);
})
}Server-side HTML rendering with Tera templates (requires protocol-view feature).
use rustapi_rs::protocol::view::{Templates, TemplatesConfig};
#[tokio::main]
async fn main() {
let templates = Templates::new(TemplatesConfig {
directory: "templates".into(),
extension: "html".into(),
}).expect("Failed to load templates");
RustApi::new()
.state(templates)
.route("/", get(home))
.run("0.0.0.0:8080")
.await
}use rustapi_rs::protocol::view::{Templates, View};
#[rustapi_rs::get("/")]
async fn home(templates: Templates) -> View<()> {
View::new(&templates, "index.html", ())
}
#[derive(Serialize)]
struct UserData {
name: String,
email: String,
}
#[rustapi_rs::get("/user/{id}")]
async fn user_page(
templates: Templates,
Path(id): Path<u64>,
) -> View<UserData> {
let user = UserData {
name: "Alice".into(),
email: "alice@example.com".into(),
};
View::new(&templates, "user.html", user)
}use rustapi_rs::protocol::view::{Templates, View, ContextBuilder};
#[rustapi_rs::get("/dashboard")]
async fn dashboard(templates: Templates) -> View<DashboardData> {
let data = get_dashboard_data();
View::with_context(&templates, "dashboard.html", data, |ctx| {
ctx.insert("title", &"Dashboard");
ctx.insert("year", &2024);
ctx.insert("nav_items", &vec!["Home", "Users", "Settings"]);
})
}templates/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
</head>
<body>
<nav>{% block nav %}{% endblock %}</nav>
<main>{% block content %}{% endblock %}</main>
</body>
</html>templates/user.html:
{% extends "base.html" %}
{% block title %}{{ name }} - My App{% endblock %}
{% block content %}
<div class="user-profile">
<h1>{{ name }}</h1>
<p>Email: {{ email }}</p>
{% if posts %}
<h2>Posts</h2>
<ul>
{% for post in posts %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}| Feature | Syntax | Description |
|---|---|---|
| Variables | {{ name }} |
Output variable |
| Filters | {{ name | upper }} |
Transform values |
| Conditionals | {% if x %}...{% endif %} |
Conditional rendering |
| Loops | {% for x in items %} |
Iteration |
| Inheritance | {% extends "base.html" %} |
Template inheritance |
| Blocks | {% block name %} |
Overridable sections |
| Includes | {% include "partial.html" %} |
Include templates |
| Macros | {% macro name() %} |
Reusable snippets |
| Filter | Example | Description |
|---|---|---|
upper |
{{ name | upper }} |
UPPERCASE |
lower |
{{ name | lower }} |
lowercase |
capitalize |
{{ name | capitalize }} |
Capitalize |
trim |
{{ text | trim }} |
Remove whitespace |
length |
{{ items | length }} |
Array/string length |
default |
{{ x | default(value="N/A") }} |
Default value |
date |
{{ dt | date(format="%Y-%m-%d") }} |
Date formatting |
json_encode |
{{ obj | json_encode }} |
JSON string |
#[rustapi_rs::get("/user/{id}")]
async fn user_page(
templates: Templates,
Path(id): Path<u64>,
) -> Result<View<UserData>> {
let user = find_user(id)
.ok_or_else(|| ApiError::not_found("User not found"))?;
Ok(View::new(&templates, "user.html", user))
}use rustapi_testing::{TestServer, Matcher};
#[tokio::test]
async fn test_api() {
let server = TestServer::new(app()).await;
let response = server
.get("/users/1")
.send()
.await;
response
.assert_status(200)
.assert_json(Matcher::object()
.field("id", 1)
.field("name", Matcher::string()));
}use rustapi_testing::Expectation;
Expectation::new()
.method("POST")
.path("/users")
.body_json(json!({ "name": "Alice" }))
.expect_status(201)
.expect_header("Location", "/users/1");use rustapi_rs::test::TestClient;
#[tokio::test]
async fn test_get_user() {
let app = RustApi::new()
.route("/users/{id}", get(get_user));
let client = TestClient::new(app);
// GET request
let res = client.get("/users/1").send().await;
assert_eq!(res.status(), 200);
// Parse JSON
let user: User = res.json().await;
assert_eq!(user.id, 1);
}#[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() })
.send()
.await;
assert_eq!(res.status(), 201);
}#[tokio::test]
async fn test_with_auth() {
let client = TestClient::new(app);
let res = client
.get("/protected")
.header("Authorization", "Bearer token123")
.send()
.await;
assert_eq!(res.status(), 200);
}#[tokio::test]
async fn test_with_mock() {
let mock_db = MockDb::new();
mock_db.insert(User { id: 1, name: "Test".into() });
let app = RustApi::new()
.state(mock_db)
.route("/users/{id}", get(get_user));
let client = TestClient::new(app);
// ...
}pub struct ApiError {
pub status: u16,
pub error_type: String,
pub message: String,
pub error_id: String,
pub fields: Option<Vec<FieldError>>,
}ApiError::bad_request("message") // 400
ApiError::unauthorized("message") // 401
ApiError::forbidden("message") // 403
ApiError::not_found("message") // 404
ApiError::method_not_allowed("message") // 405
ApiError::conflict("message") // 409
ApiError::unprocessable("message") // 422
ApiError::too_many_requests("message") // 429
ApiError::internal("message") // 500ApiError::new(StatusCode::IM_A_TEAPOT, "teapot", "I'm a teapot")use rustapi_rs::prelude::*;
async fn handler() -> Result<Json<User>> {
let user = db.find(1)
.ok_or(ApiError::not_found("User not found"))?;
Ok(Json(user))
}Set RUSTAPI_ENV=production to mask internal errors:
// Development: Full error details
// Production: "Internal server error" + error_id (details in logs)| Variable | Default | Description |
|---|---|---|
RUSTAPI_ENV |
development |
production masks errors |
RUSTAPI_LOG |
info |
Log level |
RUSTAPI_BODY_LIMIT |
1048576 |
Max body size (bytes) |
RUSTAPI_REQUEST_TIMEOUT |
30 |
Request timeout (seconds) |
RustApi::new()
.body_limit(5 * 1024 * 1024) // 5 MB
.request_timeout(Duration::from_secs(60))
.run("0.0.0.0:8080")
.await[dependencies]
rustapi-rs = { version = "0.1.335", features = ["full"] }| Feature | Description |
|---|---|
core |
Default stable core (core-openapi, core-tracing) |
core-openapi |
OpenAPI + docs endpoint support |
core-tracing |
Tracing middleware and instrumentation |
core-simd-json |
2-4x faster JSON parsing |
core-cookies |
Cookie extraction |
protocol-toon |
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 |
Rate limiting |
extras-replay |
Request replay tooling |
full |
core + protocol-all + extras-all |
Process tasks asynchronously with rustapi-jobs.
use rustapi_jobs::{Job, JobQueue, MemoryBackend};
// Define a job
#[derive(Serialize, Deserialize)]
struct SendEmailJob {
to: String,
subject: String,
}
// Create queue with in-memory backend (dev)
let queue = JobQueue::new(MemoryBackend::new());
// Enqueue a job
queue.push(Job::new("send_email", SendEmailJob {
to: "user@example.com".into(),
subject: "Welcome!".into(),
})).await?;
// Process jobs
queue.process(|job| async move {
// Handle job based on type
Ok(())
}).await;use rustapi_jobs::{JobQueue, RedisBackend};
let backend = RedisBackend::new("redis://localhost:6379").await?;
let queue = JobQueue::new(backend);use rustapi_jobs::{JobQueue, PostgresBackend};
let backend = PostgresBackend::new("postgres://localhost/jobs").await?;
let queue = JobQueue::new(backend);Handle large uploads efficiently without buffering.
use rustapi_rs::prelude::*;
use rustapi_core::stream::StreamBody;
#[rustapi_rs::post("/upload")]
async fn upload(body: StreamBody) -> Result<Json<UploadResult>, ApiError> {
let mut total_size = 0;
while let Some(chunk) = body.next().await {
let chunk = chunk?;
total_size += chunk.len();
// Process chunk without holding entire body in memory
}
Ok(Json(UploadResult { size: total_size }))
}Track user actions for compliance (GDPR, SOC2).
use rustapi_extras::audit::{AuditStore, MemoryStore, AuditEvent};
// Create audit store
let store = MemoryStore::new();
// Log an event
store.log(AuditEvent::new("user.login")
.user_id("user-123")
.ip_address("192.168.1.1")
.metadata(json!({ "browser": "Chrome" }))
).await?;
// Query events
let events = store.query()
.user_id("user-123")
.action("user.*")
.since(yesterday)
.execute()
.await?;rustapi-rs = { version = "0.1.335", features = ["core-simd-json"] }2-4x faster JSON parsing.
// Good: Single allocation
let db = Arc::new(RwLock::new(Vec::with_capacity(1000)));
// Avoid: Growing allocations
let db = Arc::new(RwLock::new(Vec::new()));// Faster (no allocation)
async fn health() -> &'static str {
"OK"
}
// Slower (allocates)
async fn health() -> String {
"OK".to_string()
}// Good: Single query
let users = db.query("SELECT * FROM users WHERE id IN ($1)", &[ids]).await;
// Avoid: N queries
for id in ids {
let user = db.query("SELECT * FROM users WHERE id = $1", &[id]).await;
}- Always validate input — Use
ValidatedJson<T> - Set body limits — Prevent DoS via large payloads
- Use HTTPS in production — Terminate TLS at load balancer
- Rotate JWT secrets — Store in environment variables
- Enable rate limiting — Prevent brute force attacks
- Mask errors in production — Set
RUSTAPI_ENV=production
For more examples, see the examples directory.