# AT-Jet Architecture Rationale
## Why AT-Jet Exists
AT-Jet is **not** a simple wrapper around axum and protobuf. It exists to solve a specific problem: **building production-grade HTTP APIs for global mobile users with Protocol Buffers**.
### The Problem
When building mobile APIs, teams face a choice:
```
Option A: JSON + REST
├── Easy to debug (human-readable)
├── Broad tooling support
└── ❌ 3-10x larger payloads than protobuf
└── ❌ No schema evolution guarantees
└── ❌ CPU-intensive parsing on mobile devices
Option B: gRPC
├── Built-in protobuf support
├── Strong typing
└── ❌ Poor CDN compatibility
└── ❌ HTTP/2 only (problematic on some networks)
└── ❌ Complex for simple request/response patterns
Option C: HTTP + Protobuf (DIY)
├── Best of both worlds
└── ❌ Significant boilerplate per project
└── ❌ Inconsistent error handling across projects
└── ❌ No established patterns for team
```
**AT-Jet chooses Option C and eliminates its downsides.**
### Our Users
AT-Jet targets a specific user profile:
```
┌─────────────────────────────────────────────────────────────────────┐
│ Global Mobile Application │
│ ├── 100M+ users across continents │
│ ├── Varying network conditions (2G to 5G) │
│ ├── Battery-conscious mobile devices │
│ └── Need: Small payloads, efficient parsing, CDN caching │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────┐
│ CDN │ ← HTTP/1.1 compatible, cacheable
└────┬────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AT-Jet API Layer │
│ ├── HTTP/1.1 + HTTP/2 support │
│ ├── Protobuf serialization │
│ ├── Gzip compression │
│ └── Standard REST semantics (GET, POST, PUT, DELETE) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Backend Services (ZUS-RS) │
│ └── Long-lived connections for internal RPC │
└─────────────────────────────────────────────────────────────────────┘
```
## What AT-Jet Provides (Beyond a Wrapper)
### 1. Type-Safe Protobuf Integration
**Without AT-Jet** (raw axum + prost):
```rust
// Every handler needs this boilerplate
async fn create_user(body: Bytes) -> impl IntoResponse {
// Manual content-type checking
// Manual size validation
// Manual protobuf decoding
// Manual error conversion
// Manual response encoding
// ... 30+ lines of repeated code
}
```
**With AT-Jet**:
```rust
async fn create_user(
ProtobufRequest(req): ProtobufRequest<CreateUserRequest>
) -> ProtobufResponse<User> {
ProtobufResponse::ok(User { id: 1, name: req.name })
}
```
This is not just convenience—it's **correctness by construction**:
- Content-type validation is automatic
- Body size limits are enforced
- Decoding errors are handled uniformly
- Response headers are set correctly
### 2. Unified Error Handling
Every Protobuf API needs consistent error responses. AT-Jet provides:
```rust
pub enum JetError {
BadRequest(String), // 400
Unauthorized(String), // 401
Forbidden(String), // 403
NotFound(String), // 404
InvalidContentType { .. }, // 415
BodyTooLarge { .. }, // 413
Internal(String), // 500
// ...
}
```
All errors automatically convert to appropriate HTTP status codes and can be serialized to protobuf error responses.
### 3. Production-Ready Defaults
AT-Jet includes what production mobile APIs need:
| Gzip compression | 50-70% smaller responses over the wire |
| Request size limits | DoS protection (default 10MB) |
| Tracing initialization | Unified setup with Jaeger/OpenTelemetry support |
| Smart tracing | Health/metrics endpoints don't flood logs |
| Prometheus metrics | HTTP request instrumentation out of the box |
| JWT authentication | HMAC-based token validation middleware |
| Session management | In-memory sessions with TTL and idle timeout |
| Graceful shutdown | Clean Ctrl+C handling for k8s |
| CORS support | Browser-based debugging tools |
| Timeout middleware | Prevent hanging requests |
### 4. Client Library Included
Most "server frameworks" stop at the server. AT-Jet provides a matching client:
```rust
let client = JetClient::new("https://api.example.com")?;
// Type-safe request/response
let user: User = client.post("/api/user", &create_request).await?;
```
This ensures:
- Consistent content-type handling
- Matching serialization/deserialization
- Shared error types between client and server
- Team can use the same patterns for service-to-service calls
### 5. AT Team Conventions Embedded
AT-Jet is pre-configured with our team's coding standards:
```rust
// Prevents panics in production
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
```
Plus:
- Nightly rustfmt configuration
- Standard error handling patterns
- Consistent project structure
## Architecture Decisions
### Decision 0: Protobuf Over JSON
**The Problem: JSON Version URL Nightmare**
With JSON APIs, schema changes often require new API versions:
```
Month 1: /api/v1/getUser → { "name": "John", "email": "john@x.com" }
Month 3: /api/v2/getUser → { "name": "John", "email": "...", "age": 25 }
Month 6: /api/v3/getUser → { "name": "John", "emailAddress": "...", "age": 25 }
Month 12: /api/v1, /v2, /v3, /v4, /v5... (maintenance nightmare)
```
Why? Because JSON uses **field names** on the wire. Renaming, adding, or removing fields can break clients.
**Protobuf Solution: Field Numbers**
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Protobuf Schema Evolution │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ message User { │
│ string name = 1; // Field NUMBER, not name, is on wire │
│ string email = 2; │
│ int32 age = 3; // Added later - old clients ignore it │
│ string phone = 4; // Added later - old clients ignore it │
│ } │
│ │
│ Wire format: [1: "John"] [2: "john@x.com"] [3: 25] [4: "123456"] │
│ │
│ • Old client + new server: client ignores unknown fields ✓ │
│ • New client + old server: missing fields get default value ✓ │
│ • ONE endpoint forever: /api/getUser (no /v1, /v2, /v3...) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
**JSON vs Protobuf Comparison**
| Add new field | May break old clients | Old clients ignore it ✓ |
| Remove field | Breaks clients expecting it | Clients use default ✓ |
| Rename field | Must version URL or break clients | Rename freely, number stays ✓ |
| Version URLs | /v1, /v2, /v3... forever | One URL forever ✓ |
| Payload size | Large (names in every message) | 3-10x smaller ✓ |
| Parse speed | Slow (string parsing) | Fast (binary) ✓ |
| Type safety | Runtime errors | Compile-time errors ✓ |
| Debugging | Human readable ✓ | Need tools |
**AT-Jet's Approach: Best of Both Worlds**
```
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ Production: Protobuf (efficient, type-safe, evolvable) │
│ Debugging: JSON with debug key (human-readable) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Mobile App (Production) │ │
│ │ Content-Type: application/x-protobuf │ │
│ │ → Fast, small, schema evolution works │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Developer (Debugging) │ │
│ │ Content-Type: application/json │ │
│ │ X-Debug-Format: <debug-key> │ │
│ │ → Human-readable, easy to inspect │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
This gives you:
- **Production efficiency** - Protobuf for mobile clients
- **Schema evolution** - No more version URL nightmare
- **Developer experience** - JSON available for debugging/testing
- **Safety** - JSON requires debug key, preventing accidental production use
---
### Decision 1: HTTP/1.1 + HTTP/2 (Not gRPC)
**Why not gRPC?**
| CDN caching | ❌ Difficult | ✅ Standard HTTP caching |
| Network compatibility | HTTP/2 only | HTTP/1.1 + HTTP/2 |
| Browser debugging | Requires special tools | Standard browser devtools |
| Load balancer support | Specialized L7 | Any HTTP LB |
| Complexity | Bidirectional streams | Simple request/response |
For mobile APIs with simple request/response patterns, HTTP + Protobuf provides better infrastructure compatibility without sacrificing the benefits of protobuf.
#### Why gRPC Has Poor CDN Compatibility
CDN caching is critical for mobile apps serving global users - it reduces latency and server load. gRPC struggles with CDNs for several reasons:
**1. POST for Everything (Not Cacheable)**
```
REST/AT-Jet: gRPC:
GET /users/123 POST /UserService/GetUser
│ │
▼ ▼
CDN sees GET request ──▶ CACHEABLE CDN sees POST request ──▶ NOT CACHED
```
gRPC uses POST for ALL operations, including reads. CDNs don't cache POST requests by default because POST typically means "create/modify data."
**2. No Natural Cache Keys**
```
REST/AT-Jet:
URL: /api/users/123?fields=name,email
└────────────────────────────────┘
Cache Key (obvious)
gRPC:
URL: /UserService/GetUser
Body: <binary: id=123, fields=[name,email]>
└─────────────────────────────────────┘
CDN cannot inspect binary body to form cache key
```
**3. gRPC-Specific Binary Framing**
gRPC adds a 5-byte prefix to every message that CDNs don't understand:
```
Standard HTTP Response: gRPC Response:
┌─────────────────────┐ ┌─────────────────────┐
│ HTTP Headers │ │ HTTP Headers │
├─────────────────────┤ ├─────────────────────┤
│ Protobuf Body │ │ 1 byte: compressed? │ ← gRPC framing
│ (pure binary) │ │ 4 bytes: length │ ← gRPC framing
└─────────────────────┘ │ Protobuf Body │
├─────────────────────┤
│ HTTP Trailers │ ← Status code here!
└─────────────────────┘
```
**4. HTTP Trailers for Status**
gRPC sends status codes in HTTP trailers (after the body), not in HTTP status codes:
```
REST/AT-Jet: gRPC:
HTTP/1.1 404 Not Found HTTP/2 200 OK
...body...
grpc-status: 5 (NOT_FOUND) ← Trailer
```
Many CDNs and proxies either don't support trailers or strip them, breaking gRPC error handling.
**5. HTTP/2 Requirement**
gRPC requires HTTP/2. While modern CDNs support HTTP/2, some edge cases cause issues:
- Origin connections may fall back to HTTP/1.1
- Some corporate proxies don't support HTTP/2
- Mobile networks in developing regions may have HTTP/2 issues
**Summary: CDN Compatibility Comparison**
| Read requests | POST (not cacheable) | GET (cacheable by default) |
| Cache-Control | Non-standard | Standard HTTP headers work |
| Response body | 5-byte prefix + proto | Pure protobuf (or JSON) |
| Status codes | In HTTP trailers | Standard HTTP status codes |
| Cache key | Requires body inspection | URL + query params |
| CDN support | Needs gRPC-aware proxy | Any HTTP CDN works |
### Decision 2: Axum as Foundation
Why axum over alternatives?
| **axum** | Type-safe extractors, tower ecosystem, active development | Newer |
| actix-web | Mature, fast | Actor model complexity |
| warp | Composable filters | Less intuitive |
| rocket | Declarative | Sync-first design |
Axum's extractor pattern maps perfectly to our Protobuf request/response model.
### Decision 3: Separate from ZUS-RS
AT-Jet is a separate project from ZUS-RS because:
1. **Different transport**: HTTP vs TCP
2. **Different users**: Mobile clients vs backend services
3. **Different dependencies**: axum/reqwest vs tokio-only
4. **Different concerns**: CDN compatibility vs low latency
However, they work together:
```
Mobile Client ──HTTP/Protobuf──▶ AT-Jet API ──ZUS-RS──▶ Backend Services
```
### Decision 4: Convention Over Configuration
AT-Jet makes opinionated choices:
| Content-Type | `application/x-protobuf` | Industry standard |
| Max body size | 10MB | Reasonable for mobile uploads |
| Default timeout | 30s | Matches common mobile timeouts |
| Compression | gzip | Universal support |
These can be overridden, but sensible defaults reduce configuration burden.
### Decision 5: Dual-Format with Debug Key Authorization
AT-Jet supports both Protobuf (production) and JSON (debugging), but **JSON requires explicit authorization** via the `X-Debug-Format` header.
**Why not allow unrestricted JSON?**
```
┌─────────────────────────────────────────────────────────────────────┐
│ Schema Evolution Problem │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Protobuf: JSON: │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ message User { │ │ { "name": "John" } │ │
│ │ string name = 1; │ │ │ │
│ │ int32 age = 2; │ ← NEW │ { "name": "John", │ │
│ │ } │ FIELD │ "age": 25 } │ ← BREAKS │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ Old client reads new data: │
│ - Protobuf: ✅ Unknown field 2 is ignored │
│ - JSON: ❌ May fail if schema validation is strict │
│ │
│ New client reads old data: │
│ - Protobuf: ✅ Missing field 2 uses default (0) │
│ - JSON: ❌ "age" key missing, null handling varies │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**The debug key approach:**
| **Allow JSON freely** | Easy debugging | Clients may use in production, breaking on schema changes |
| **Disable JSON completely** | Safe | Poor developer experience |
| **Debug key authorization** ✅ | Safe + debuggable | Requires key distribution |
We chose debug key authorization because:
1. **Production safety** - Clients can't accidentally use JSON
2. **Developer experience** - JSON available for debugging/testing
3. **Auditability** - Keys identify who's using JSON
4. **Flexibility** - Keys can be rotated or revoked
**Implementation:**
```rust
// Configure at startup (empty = JSON disabled)
configure_debug_keys(vec![
"alice-dev-key".to_string(),
"bob-dev-key".to_string(),
]);
// Handler uses dual-format types
async fn create_user(
ApiRequest { body, format }: ApiRequest<CreateUserRequest>
) -> ApiResponse<User> {
ApiResponse::ok(format, user)
}
// Client must provide valid key for JSON
// X-Debug-Format: alice-dev-key
```
**Format selection flow:**
```
Request arrives
│
▼
┌─────────────────────────────────────┐
│ Content-Type: application/json? │
└─────────────────────────────────────┘
│ Yes │ No
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ X-Debug-Format │ │ Decode as │
│ header valid? │ │ Protobuf │
└─────────────────┘ └─────────────────┘
│ Yes │ No
▼ ▼
┌────────┐ ┌────────────┐
│ Decode │ │ 415 Error │
│ as JSON│ │ (rejected) │
└────────┘ └────────────┘
```
## What AT-Jet is NOT
1. **Not a general-purpose web framework** - Use axum directly for HTML, JSON APIs
2. **Not a replacement for ZUS-RS** - Different use cases (see [BEST_PRACTICES.md](../../mirror_projects/zus-rs/docs/BEST_PRACTICES.md))
3. **Not a gRPC alternative** - If you need streaming, use gRPC
4. **Not opinionated about proto definitions** - Bring your own .proto files
## Comparison with Alternatives
### vs. Using axum + prost directly
| Lines of code per handler | ~30 | ~5 |
| Error handling | Manual | Unified |
| Content negotiation | Manual | Automatic |
| Client library | Write your own | Included |
| JWT auth middleware | Write your own | Built-in (`jwt` feature) |
| Tracing setup | Manual | `init_tracing()` with sensible defaults |
| Metrics | Manual | Built-in (`metrics` feature) |
| Graceful shutdown | Manual | `serve_with_shutdown()` |
| Team conventions | External docs | Embedded |
### vs. tonic (gRPC)
| Protocol | gRPC/HTTP2 | HTTP/1.1+2 |
| CDN support | Poor | Excellent |
| Service definition | Required .proto | Optional |
| Learning curve | gRPC concepts | REST concepts |
| Streaming | Bidirectional | Request/Response only |
### vs. JSON APIs
| Payload size | Larger | 3-10x smaller |
| Parse speed | Slower | Faster |
| Schema evolution | Manual | Built-in |
| Type safety | Runtime | Compile-time |
| Debugging | Human-readable | JSON available with debug key |
Note: AT-Jet provides JSON format for debugging/testing when a valid debug key is provided via `X-Debug-Format` header. This gives you the best of both worlds: efficient Protobuf in production, human-readable JSON during development.
## Future Roadmap
### Phase 1: Core Framework (v0.1–v0.4)
- [x] Server with Protobuf extractors
- [x] Client with type-safe requests
- [x] Basic middleware (CORS, compression, tracing)
- [x] Error handling framework
- [x] Dual-format support (Protobuf + JSON with debug key authorization)
### Phase 2: Production Hardening (v0.5–v0.6, current)
- [ ] Rate limiting middleware
- [x] Authentication middleware (Basic Auth)
- [x] JWT authentication — optional `jwt` feature with HMAC-based validation and middleware layer
- [x] Session management — optional `session` feature with in-memory store, TTL, and idle timeout
- [ ] Request ID propagation
- [x] Metrics (Prometheus) — optional `metrics` feature with HTTP instrumentation and scrape endpoint
- [x] Smart tracing — health/metrics endpoints automatically downgraded to TRACE level
- [x] Tracing initialization — unified setup with optional Jaeger/OpenTelemetry (`tracing-otel` feature)
- [x] Graceful shutdown — `serve_with_shutdown()` for clean Ctrl+C handling
- [x] Startup banner — pre-tracing service info output for k8s environments
### Phase 3: Developer Experience
- [ ] OpenAPI spec generation from proto
- [ ] Proto-to-client codegen
- [ ] Testing utilities
- [ ] Documentation generator
### Phase 4: Advanced Features
- [ ] Response caching with ETags
- [ ] Partial responses (field masks)
- [ ] Batch endpoints
- [ ] Optional ZUS-RS integration for backend calls
## Conclusion
AT-Jet exists because:
1. **Mobile users deserve efficient APIs** - Protobuf reduces bandwidth and battery usage
2. **Teams deserve consistency** - Shared patterns reduce bugs and onboarding time
3. **Infrastructure deserves compatibility** - HTTP works everywhere
4. **Developers deserve productivity** - Less boilerplate, more features
It's not a wrapper—it's an **opinionated, production-ready foundation** for mobile API development at AT.