# 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:**

| Method | Description |
|--------|-------------|
| `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):

| Format | Use Case | Characteristics |
|--------|----------|-----------------|
| **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

| Method | Description |
|--------|-------------|
| `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

| Method | Description |
|--------|-------------|
| `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

| Method | Description |
|--------|-------------|
| `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

| Extractor | Description |
|-----------|-------------|
| `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

| Response | Description |
|----------|-------------|
| `ProtobufResponse<T>` | Protobuf-only response |
| `ApiResponse<T>` | Protobuf or JSON response |

### Error Types

| Error | HTTP Status |
|-------|-------------|
| `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
