# AT-Jet User Guide
Comprehensive guide to building HTTP + Protobuf APIs with AT-Jet.
## Table of Contents
1. [Installation](#installation)
2. [Server](#server)
- [Basic Server Setup](#basic-server-setup)
- [Routing](#routing)
- [Request Extractors](#request-extractors)
- [Response Types](#response-types)
- [Error Handling](#error-handling)
- [Middleware](#middleware)
3. [Client](#client)
- [Basic Client Usage](#basic-client-usage)
- [Client Builder](#client-builder)
- [Error Handling](#client-error-handling)
4. [Dual Format Support](#dual-format-support)
- [Why Dual Format?](#why-dual-format)
- [Server Configuration](#server-configuration)
- [Dual Format Handlers](#dual-format-handlers)
- [Client JSON Requests](#client-json-requests)
5. [Metrics](#metrics)
- [Enabling Metrics](#enabling-metrics)
- [Metrics Configuration](#metrics-configuration)
- [Custom Histogram Buckets](#custom-histogram-buckets)
6. [Tracing Initialization](#tracing-initialization)
- [Basic Tracing Setup](#basic-tracing-setup)
- [Jaeger Integration](#jaeger-integration)
7. [JWT Authentication](#jwt-authentication)
- [JWT Configuration](#jwt-configuration)
- [JWT Middleware](#jwt-middleware)
- [Subject Validation](#subject-validation)
8. [Session Management](#session-management)
- [Creating Sessions](#creating-sessions)
- [Session Cleanup](#session-cleanup)
9. [Server Utilities](#server-utilities)
- [Graceful Shutdown](#graceful-shutdown)
- [Startup Banner](#startup-banner)
10. [Protobuf Integration](#protobuf-integration)
- [Build Setup](#build-setup)
- [Schema Design Tips](#schema-design-tips)
11. [Best Practices](#best-practices)
12. [API Reference](#api-reference)
---
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
at-jet = "0.6"
tokio = { version = "1", features = ["full"] }
prost = "0.13"
[build-dependencies]
prost-build = "0.13"
```
### System Requirements
- Rust 1.75 or later (required by axum 0.7)
- `protoc` (Protocol Buffers compiler)
```bash
# macOS
brew install protobuf
# Ubuntu/Debian
sudo apt install protobuf-compiler
# Windows (with chocolatey)
choco install protoc
```
---
## Server
### Basic Server Setup
```rust
use at_jet::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let server = JetServer::new()
.route("/health", get(health_check))
.route("/api/users", get(list_users).post(create_user))
.route("/api/users/:id", get(get_user).put(update_user).delete(delete_user))
.with_cors()
.with_compression();
server.serve("0.0.0.0:8080").await?;
Ok(())
}
async fn health_check() -> &'static str {
"OK"
}
```
### Routing
AT-Jet uses axum's routing system. Common patterns:
```rust
let server = JetServer::new()
// Single method
.route("/users", get(list_users))
// Multiple methods on same path
.route("/users", get(list_users).post(create_user))
// Path parameters
.route("/users/:id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_user_post))
// Nested routes
.nest("/api/v1", api_v1_router())
// Fallback for unmatched routes
.fallback(not_found_handler);
```
### Request Extractors
#### ProtobufRequest - Decode Protobuf Body
```rust
async fn create_user(
ProtobufRequest(req): ProtobufRequest<CreateUserRequest>,
) -> ProtobufResponse<User> {
// req is your decoded protobuf message
let user = User {
id: 1,
name: req.name,
email: req.email,
};
ProtobufResponse::created(user)
}
```
#### Path - Extract URL Parameters
```rust
async fn get_user(Path(id): Path<i32>) -> ProtobufResponse<User> {
// id extracted from /users/:id
ProtobufResponse::ok(user)
}
// Multiple parameters
async fn get_post(
Path((user_id, post_id)): Path<(i32, i32)>,
) -> ProtobufResponse<Post> {
// Extracted from /users/:user_id/posts/:post_id
ProtobufResponse::ok(post)
}
```
#### Query - Extract Query Parameters
```rust
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<i32>,
limit: Option<i32>,
}
async fn list_users(Query(params): Query<Pagination>) -> ProtobufResponse<ListUsersResponse> {
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(20);
// Use page and limit for pagination
ProtobufResponse::ok(response)
}
```
#### Headers - Access Request Headers
```rust
use axum::http::HeaderMap;
async fn authenticated_endpoint(headers: HeaderMap) -> ProtobufResponse<Data> {
if let Some(auth) = headers.get("Authorization") {
// Validate token
}
ProtobufResponse::ok(data)
}
```
### Response Types
#### ProtobufResponse - Standard Protobuf Response
```rust
// 200 OK
ProtobufResponse::ok(message)
// 201 Created
ProtobufResponse::created(message)
// 204 No Content
ProtobufResponse::<()>::no_content()
// Custom status code
ProtobufResponse::new(StatusCode::ACCEPTED, message)
```
#### Error Responses
```rust
use at_jet::error::JetError;
async fn get_user(Path(id): Path<i32>) -> Result<ProtobufResponse<User>, JetError> {
let user = find_user(id).ok_or_else(|| JetError::NotFound("User not found".into()))?;
Ok(ProtobufResponse::ok(user))
}
```
### Error Handling
AT-Jet provides structured error types:
```rust
use at_jet::error::JetError;
// Available error types:
JetError::BadRequest("Invalid input".into()) // 400
JetError::Unauthorized("Not authenticated".into()) // 401
JetError::Forbidden("Access denied".into()) // 403
JetError::NotFound("Resource not found".into()) // 404
JetError::Internal("Server error".into()) // 500
// Errors automatically convert to HTTP responses
async fn create_user(
ProtobufRequest(req): ProtobufRequest<CreateUserRequest>,
) -> Result<ProtobufResponse<User>, JetError> {
if req.name.is_empty() {
return Err(JetError::BadRequest("Name is required".into()));
}
let user = save_user(req).await
.map_err(|e| JetError::Internal(e.to_string()))?;
Ok(ProtobufResponse::created(user))
}
```
### Middleware
AT-Jet provides both built-in middleware and support for custom tower-compatible layers.
#### Built-in Middleware
```rust
use at_jet::prelude::*;
let server = JetServer::new()
.route("/api/users", get(list_users))
.with_cors() // Enable CORS for browser access
.with_compression() // Enable gzip compression
.with_tracing(); // Enable request/response tracing
```
**Available built-in middleware:**
| `with_cors()` | Enable permissive CORS (allows any origin, method, headers) |
| `with_compression()` | Enable gzip response compression |
| `with_tracing()` | Enable smart HTTP tracing (health/metrics at TRACE level, all others at INFO) |
| `with_metrics(guard, path)` | Enable Prometheus metrics endpoint + HTTP instrumentation (requires `metrics` feature) |
#### Request Tracing (Smart)
`with_tracing()` uses a `SmartSpanMaker` that automatically assigns log levels based on path:
- **`/health`, `/metrics`** endpoints → `TRACE` level (hidden from normal logs)
- **All other paths** → `INFO` level (visible in normal logging)
This prevents health checks and metrics scrapes from flooding your logs.
To see tracing output, initialize tracing before starting the server. The recommended approach is `init_tracing()`:
```rust
use at_jet::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Recommended: use init_tracing() for unified setup
let _guard = init_tracing(&TracingConfig {
level: "debug".to_string(),
env_filter_conf: vec!["at_jet=debug".to_string()],
..Default::default()
});
let server = JetServer::new()
.route("/api/users", get(list_users))
.with_tracing();
server.serve_with_shutdown("0.0.0.0:8080").await?;
Ok(())
}
```
Or override with `RUST_LOG` environment variable:
```bash
RUST_LOG=debug cargo run
```
See [Tracing Initialization](#tracing-initialization) for full configuration options including Jaeger integration.
You can also use `SmartSpanMaker` or `smart_trace_layer()` directly on sub-routers:
```rust
use at_jet::middleware::smart_trace_layer;
let api_router = Router::new()
.route("/users", get(list_users))
.layer(smart_trace_layer());
```
#### Custom Middleware with `.layer()`
Use the `.layer()` method to add any tower-compatible middleware:
```rust
use at_jet::prelude::*;
use std::time::Duration;
use tower_http::timeout::TimeoutLayer;
let server = JetServer::new()
.route("/api/users", get(list_users))
.layer(TimeoutLayer::new(Duration::from_secs(30))) // Request timeout
.with_cors()
.with_compression();
```
#### Custom Authentication Middleware
> **Tip:** For JWT-based authentication, use the built-in [`JwtAuthLayer`](#jwt-authentication) instead of writing custom middleware. The example below is for non-JWT auth patterns.
```rust
use at_jet::prelude::*;
use axum::{
extract::Request,
http::{header, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
};
async fn auth_middleware(request: Request, next: Next) -> Response {
// Extract Authorization header
let auth_header = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|h| h.to_str().ok());
match auth_header {
Some(token) if token.starts_with("Bearer ") => {
// Validate token here
let token = &token[7..];
if is_valid_token(token) {
next.run(request).await
} else {
(StatusCode::UNAUTHORIZED, "Invalid token").into_response()
}
}
_ => (StatusCode::UNAUTHORIZED, "Missing authorization").into_response(),
}
}
fn is_valid_token(token: &str) -> bool {
// Your token validation logic
token == "valid-token"
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let server = JetServer::new()
// Public routes (no auth)
.route("/health", get(health_check))
.route("/api/public", get(public_endpoint))
// Apply auth middleware to protected routes
.layer(middleware::from_fn(auth_middleware))
// Protected routes (require auth)
.route("/api/users", get(list_users))
.route("/api/admin", get(admin_endpoint))
.with_cors();
server.serve("0.0.0.0:8080").await?;
Ok(())
}
```
**Note:** Middleware layers apply to routes defined **before** the `.layer()` call in the builder chain. Routes defined after `.layer()` are not affected by that layer.
#### Request Logging Middleware
```rust
use at_jet::prelude::*;
use axum::{extract::Request, middleware::Next, response::Response};
use std::time::Instant;
async fn logging_middleware(request: Request, next: Next) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let start = Instant::now();
let response = next.run(request).await;
let duration = start.elapsed();
let status = response.status();
tracing::info!(
method = %method,
uri = %uri,
status = %status,
duration_ms = %duration.as_millis(),
"Request completed"
);
response
}
let server = JetServer::new()
.route("/api/users", get(list_users))
.layer(axum::middleware::from_fn(logging_middleware));
```
#### Rate Limiting (with tower)
```rust
use at_jet::prelude::*;
use std::time::Duration;
use tower::limit::RateLimitLayer;
let server = JetServer::new()
.route("/api/users", get(list_users))
// Limit to 100 requests per second
.layer(RateLimitLayer::new(100, Duration::from_secs(1)))
.with_cors();
```
#### Combining Multiple Layers
```rust
use at_jet::prelude::*;
use axum::middleware;
use std::time::Duration;
use tower_http::timeout::TimeoutLayer;
let server = JetServer::new()
.route("/health", get(health_check))
.route("/api/users", get(list_users).post(create_user))
.route("/api/users/:id", get(get_user))
// Add layers (applied in reverse order - last added runs first)
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(middleware::from_fn(logging_middleware))
.layer(middleware::from_fn(auth_middleware))
// Built-in middleware
.with_cors()
.with_compression()
.with_tracing();
```
**Layer execution order:** Layers are applied in reverse order. The last `.layer()` call wraps the innermost, so it executes first on requests and last on responses.
---
## Client
### Basic Client Usage
```rust
use at_jet::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = JetClient::new("http://localhost:8080")?;
// GET request
let user: User = client.get("/api/users/123").await?;
// POST request
let request = CreateUserRequest {
name: "John".into(),
email: "john@example.com".into(),
};
let created: User = client.post("/api/users", &request).await?;
// PUT request
let update = UpdateUserRequest { name: "Jane".into() };
let updated: User = client.put("/api/users/123", &update).await?;
// DELETE request
let response: DeleteResponse = client.delete("/api/users/123").await?;
Ok(())
}
```
### Client Builder
For advanced configuration:
```rust
use std::time::Duration;
let client = JetClient::builder()
.base_url("http://localhost:8080")
.timeout(Duration::from_secs(60))
.gzip(true)
.debug_key("dev-debug-key") // Enable JSON debug requests
.build()?;
```
### Client Error Handling
```rust
use at_jet::error::JetError;
async fn fetch_user(client: &JetClient, id: i32) -> Result<User, JetError> {
match client.get::<User>(&format!("/api/users/{}", id)).await {
Ok(user) => Ok(user),
Err(JetError::NotFound(_)) => {
// Handle 404
Err(JetError::NotFound("User does not exist".into()))
}
Err(e) => Err(e),
}
}
```
---
## Dual Format Support
### Why Dual Format?
AT-Jet supports both Protobuf (production) and JSON (debugging):
| **Protobuf** | Production | Small, fast, schema evolution |
| **JSON** | Debugging | Human-readable, easy inspection |
JSON requires a debug key to prevent accidental use in production, which would lose Protobuf's schema evolution guarantees.
### Server Configuration
Configure debug keys at startup:
```rust
use at_jet::dual_format::configure_debug_keys;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Configure authorized debug keys
configure_debug_keys(vec![
"alice-dev-key".to_string(),
"bob-dev-key".to_string(),
"qa-team-key".to_string(),
]);
// Empty list disables JSON completely (production default)
// configure_debug_keys(vec![]);
let server = JetServer::new()
.route("/api/users", get(list_users).post(create_user))
.with_cors();
server.serve("0.0.0.0:8080").await?;
Ok(())
}
```
### Dual Format Handlers
Use `ApiRequest` and `ApiResponse` for handlers that support both formats:
```rust
use at_jet::prelude::*;
// Your proto types must implement Serialize/Deserialize for JSON support
impl serde::Serialize for User {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where S: serde::Serializer {
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("User", 3)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("name", &self.name)?;
state.serialize_field("email", &self.email)?;
state.end()
}
}
impl<'de> serde::Deserialize<'de> for CreateUserRequest {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where D: serde::Deserializer<'de> {
#[derive(serde::Deserialize)]
struct Helper { name: String, email: String }
let h = Helper::deserialize(deserializer)?;
Ok(CreateUserRequest { name: h.name, email: h.email })
}
}
// Dual-format handler
async fn create_user(request: ApiRequest<CreateUserRequest>) -> ApiResponse<User> {
let user = User {
id: 1,
name: request.body.name.clone(),
email: request.body.email.clone(),
};
request.created(user) // Response format matches request format
}
// GET handler with format detection
async fn list_users(AcceptFormat(format): AcceptFormat) -> ApiResponse<ListUsersResponse> {
let response = ListUsersResponse { users: vec![], total: 0 };
ApiResponse::ok(format, response)
}
```
### Client JSON Requests
Use the debug client for JSON requests:
```rust
// Define JSON-compatible types
#[derive(serde::Serialize, serde::Deserialize)]
struct UserJson {
id: i32,
name: String,
email: String,
}
#[derive(serde::Serialize)]
struct CreateUserJson {
name: String,
email: String,
}
// Create client with debug key
let client = JetClient::builder()
.base_url("http://localhost:8080")
.debug_key("dev-debug-key")
.build()?;
// JSON GET request (typed)
let user: UserJson = client.get_json("/api/users/1").await?;
// JSON GET request (raw string for inspection)
let json_text = client.get_json_raw("/api/users").await?;
println!("Response: {}", json_text);
// JSON POST request
let request = CreateUserJson {
name: "John".into(),
email: "john@example.com".into(),
};
let created: UserJson = client.post_json("/api/users", &request).await?;
// JSON PUT request
let updated: UserJson = client.put_json("/api/users/1", &update).await?;
// JSON DELETE request
let response: DeleteJson = client.delete_json("/api/users/1").await?;
```
### curl Examples
```bash
# Protobuf request (production - no debug key needed)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/x-protobuf" \
-H "Accept: application/x-protobuf" \
--data-binary @request.pb
# JSON request (debugging - requires valid debug key)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Debug-Format: dev-debug-key" \
-d '{"name": "John", "email": "john@example.com"}'
# JSON request without debug key -> 415 Unsupported Media Type
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "John"}'
```
---
## Metrics
AT-Jet provides optional Prometheus metrics support behind the `metrics` feature flag.
### Enabling Metrics
Add the `metrics` feature to your `Cargo.toml`:
```toml
[dependencies]
at-jet = { version = "0.6", features = ["metrics"] }
```
### Metrics Configuration
```rust
use at_jet::prelude::*;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize metrics with default HTTP buckets
let guard = init_metrics(&MetricsConfig::default())
.ok_or_else(|| anyhow::anyhow!("Failed to initialize metrics"))?;
// Record application info (shows up as `app_info` gauge)
record_app_info("my-service", env!("CARGO_PKG_VERSION"));
let guard = Arc::new(guard);
let server = JetServer::new()
.route("/health", get(|| async { "OK" }))
.route("/api/users", get(list_users))
.with_metrics(guard, "/metrics") // Scrape endpoint + HTTP instrumentation
.with_tracing();
server.serve("0.0.0.0:8080").await?;
Ok(())
}
```
The `with_metrics()` convenience method adds:
- A Prometheus scrape endpoint at the given path
- An `HttpMetricsLayer` that automatically records:
- `http_requests_total` — counter with labels: `method`, `endpoint`, `status`
- `http_request_duration_seconds` — histogram with labels: `method`, `endpoint`
- `http_active_requests` — gauge with label: `endpoint`
Endpoint labels are normalized automatically (e.g., `/my-service/users/list` becomes `users_list`).
### Custom Histogram Buckets
Override default HTTP buckets or add application-specific bucket configurations:
```rust
let config = MetricsConfig {
enabled: true,
http_buckets: vec![0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0],
custom_buckets: vec![
("db_query_duration_seconds".into(), vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0]),
("cache_operation_duration_seconds".into(), vec![0.0005, 0.001, 0.005, 0.01, 0.05]),
],
};
let guard = init_metrics(&config);
```
To disable metrics, set `enabled: false`:
```rust
let config = MetricsConfig {
enabled: false,
..Default::default()
};
assert!(init_metrics(&config).is_none());
```
---
## Tracing Initialization
AT-Jet provides a unified tracing setup that works out of the box with sensible defaults.
### Basic Tracing Setup
`TracingConfig` and `init_tracing()` are always available (no feature flag required):
```rust
use at_jet::prelude::*;
let config = TracingConfig {
level: "info".to_string(),
format: "pretty".to_string(), // "json" for k8s
..Default::default()
};
let _guard = init_tracing(&config); // Keep guard alive!
```
The tracing guard must be kept alive for the duration of the program. Default filters suppress noisy crates (sqlx, hyper, reqwest, h2, tower, rustls → warn level).
### Jaeger Integration
Enable the `tracing-otel` feature for Jaeger support:
```toml
[dependencies]
at-jet = { version = "0.6", features = ["tracing-otel"] }
```
```rust
let config = TracingConfig {
level: "info".to_string(),
format: "json".to_string(),
jaeger_enabled: true,
jaeger_endpoint: "http://jaeger:14268/api/traces".to_string(),
service_name: "my-service".to_string(),
..Default::default()
};
let _guard = init_tracing(&config);
```
Supports both HTTP collector and UDP agent endpoints. When `tracing-otel` is disabled, Jaeger fields are accepted but ignored.
---
## JWT Authentication
AT-Jet provides JWT authentication middleware behind the `jwt` feature flag.
### JWT Configuration
```toml
[dependencies]
at-jet = { version = "0.6", features = ["jwt"] }
```
```rust
use at_jet::prelude::*;
let config = JwtConfig {
secret: "base64-encoded-hmac-secret".to_string(),
algorithm: jsonwebtoken::Algorithm::HS512, // default
};
```
The secret must be base64-encoded. If empty or invalid, the validator reports `NotConfigured`.
### JWT Middleware
Apply `JwtAuthLayer` to protect routes. On success, the subject claim is inserted into request extensions:
```rust
use at_jet::prelude::*;
use axum::Extension;
let config = JwtConfig {
secret: "base64-secret".to_string(),
..Default::default()
};
let server = JetServer::new()
.route("/api/me", get(me_handler))
.layer(JwtAuthLayer::new(&config));
async fn me_handler(Extension(subject): Extension<String>) -> String {
format!("Hello, {}", subject)
}
```
### Subject Validation
Add application-specific validation of the subject claim:
```rust
let layer = JwtAuthLayer::new(&config)
.with_subject_validator(|sub| {
sub.len() == 8 && sub.chars().all(|c| c.is_ascii_alphanumeric())
});
```
---
## Session Management
AT-Jet provides an in-memory session store behind the `session` feature flag.
### Creating Sessions
```toml
[dependencies]
at-jet = { version = "0.6", features = ["session"] }
```
```rust
use at_jet::prelude::*;
use std::collections::HashMap;
let store = SessionStore::new(28800, 900); // 8h TTL, 15min idle
// Create session with flexible attributes
let attrs = HashMap::from([
("role".into(), "admin".into()),
("email".into(), "user@example.com".into()),
]);
let token = store.create("username".into(), attrs).await;
// Validate (refreshes idle timer)
if let Some(session) = store.validate(&token).await {
println!("User: {}", session.identity);
}
// Validate without refresh (for polling endpoints)
let _ = store.validate_without_refresh(&token).await;
// Logout
store.invalidate(&token).await;
```
### Session Cleanup
Run periodic cleanup in a background task:
```rust
let store = std::sync::Arc::new(SessionStore::new(28800, 900));
let cleanup_store = store.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
loop {
interval.tick().await;
cleanup_store.cleanup_expired().await;
}
});
```
---
## Server Utilities
### Graceful Shutdown
Use `serve_with_shutdown()` for clean Ctrl+C handling:
```rust
let server = JetServer::new()
.route("/health", get(|| async { "OK" }))
.with_tracing();
server.serve_with_shutdown("0.0.0.0:8080").await?;
// Returns cleanly when Ctrl+C is received
```
### Startup Banner
Print service info to stderr before tracing is initialized:
```rust
JetServer::print_banner("my-service", env!("CARGO_PKG_VERSION"), &[
("Environment", "production"),
("Config", "spring-cloud"),
]);
```
Output:
```
======================================
my-service v1.0.0
======================================
Environment: production
Config: spring-cloud
```
---
## Protobuf Integration
### Build Setup
1. Create `proto/` directory with your `.proto` files
2. Add `build.rs`:
```rust
fn main() {
prost_build::compile_protos(
&["proto/user.proto", "proto/product.proto"],
&["proto/"],
).unwrap();
}
```
3. Include generated code in your source:
```rust
pub mod proto {
include!(concat!(env!("OUT_DIR"), "/mypackage.rs"));
}
use proto::{User, CreateUserRequest};
```
### Schema Design Tips
```protobuf
syntax = "proto3";
package myapi;
// Use clear, consistent naming
message User {
int32 id = 1; // Field numbers are the wire format
string name = 2; // Names can be renamed without breaking
string email = 3;
int64 created_at = 4; // Use int64 for timestamps (Unix epoch)
}
// Request messages should be specific
message CreateUserRequest {
string name = 1;
string email = 2;
}
message UpdateUserRequest {
int32 id = 1;
optional string name = 2; // Optional fields for partial updates
optional string email = 3;
}
// List responses include pagination
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
int32 page = 3;
int32 page_size = 4;
}
// Standard success/failure response
message DeleteResponse {
bool success = 1;
string message = 2;
}
```
**Schema Evolution Rules:**
- Never reuse field numbers
- Never change field types
- Add new fields with new numbers
- Use `optional` for fields that may not be set
- Use `reserved` for removed fields:
```protobuf
message User {
reserved 5, 6; // Field numbers no longer in use
reserved "old_field"; // Field names no longer in use
int32 id = 1;
string name = 2;
}
```
---
## Best Practices
### 1. Always Use Type-Safe Extractors
```rust
// Good - type-safe, automatic validation
async fn get_user(Path(id): Path<i32>) -> ProtobufResponse<User> { ... }
// Avoid - manual parsing
async fn get_user(req: Request) -> impl IntoResponse { ... }
```
### 2. Use Result for Fallible Operations
```rust
async fn create_user(
ProtobufRequest(req): ProtobufRequest<CreateUserRequest>,
) -> Result<ProtobufResponse<User>, JetError> {
validate(&req)?;
let user = save_user(req).await?;
Ok(ProtobufResponse::created(user))
}
```
### 3. Configure Debug Keys Appropriately
```rust
// Development/Staging: Enable debug keys
configure_debug_keys(vec!["dev-key".into(), "qa-key".into()]);
// Production: Disable or use strictly controlled keys
configure_debug_keys(vec![]); // Disable JSON completely
```
### 4. Use Compression for Large Responses
```rust
let server = JetServer::new()
.route("/api/data", get(large_data))
.with_compression(); // Gzip compression enabled
```
### 5. Set Appropriate Timeouts
```rust
let client = JetClient::builder()
.base_url("http://api.example.com")
.timeout(Duration::from_secs(30)) // Adjust based on expected response times
.build()?;
```
---
## API Reference
### JetServer
| `new()` | Create new server |
| `route(path, handler)` | Add route with handler |
| `nest(path, router)` | Nest another router at path |
| `merge(router)` | Merge another router into this server |
| `layer(layer)` | Add custom tower-compatible middleware layer |
| `with_cors()` | Enable permissive CORS (any origin/method/header) |
| `with_compression()` | Enable gzip response compression |
| `with_tracing()` | Enable smart HTTP tracing (health/metrics at TRACE level) |
| `with_metrics(guard, path)` | Enable Prometheus metrics endpoint + HTTP instrumentation (`metrics` feature) |
| `into_router()` | Get underlying axum Router for advanced customization |
| `serve(addr)` | Start server on address |
| `serve_with_shutdown(addr)` | Start server with graceful Ctrl+C shutdown |
| `print_banner(name, ver, extras)` | Print startup banner to stderr (static method) |
### JetClient
| `new(base_url)` | Create client (Protobuf only) |
| `builder()` | Create client builder |
| `get<T>(path)` | GET request, decode Protobuf |
| `post<Req, Res>(path, body)` | POST request with Protobuf |
| `put<Req, Res>(path, body)` | PUT request with Protobuf |
| `delete<T>(path)` | DELETE request, decode Protobuf |
| `get_json<T>(path)` | GET request, decode JSON |
| `post_json<Req, Res>(path, body)` | POST request with JSON |
| `put_json<Req, Res>(path, body)` | PUT request with JSON |
| `delete_json<T>(path)` | DELETE request, decode JSON |
| `get_json_raw(path)` | GET request, return raw JSON string |
### JetClientBuilder
| `base_url(url)` | Set base URL |
| `timeout(duration)` | Set request timeout |
| `gzip(enabled)` | Enable/disable gzip |
| `debug_key(key)` | Set debug key for JSON requests |
| `build()` | Build client |
### Request Extractors
| `ProtobufRequest<T>` | Decode Protobuf body |
| `ApiRequest<T>` | Decode Protobuf or JSON body |
| `AcceptFormat` | Extract requested response format |
| `Path<T>` | Extract path parameters |
| `Query<T>` | Extract query parameters |
### Response Types
| `ProtobufResponse<T>` | Protobuf-only response |
| `ApiResponse<T>` | Protobuf or JSON response |
### Error Types
| `JetError::BadRequest` | 400 |
| `JetError::Unauthorized` | 401 |
| `JetError::Forbidden` | 403 |
| `JetError::NotFound` | 404 |
| `JetError::InvalidContentType` | 415 |
| `JetError::BodyTooLarge` | 413 |
| `JetError::Internal` | 500 |
---
## Further Reading
- [Quick Start](QUICK_START.md) - Get running in 5 minutes
- [Architecture](ARCHITECTURE.md) - Design decisions and rationale
- [Examples](../examples/) - Working code examples