# Oxide
A **Rust-native, opinionated web framework** that delivers a Spring Boot-like developer experience — without runtime reflection.
Built on top of [Axum](https://github.com/tokio-rs/axum) and [Tokio](https://tokio.rs), Oxide provides convention-first project structure, a clean chainable API, and standardized patterns for routing, configuration, middleware, and shared state.
## Why Not Just Use Axum?
Axum is an incredible, high-performance routing library, but it is **not a full framework**.
When building production services with raw Axum, you repeatedly wire together the same boilerplate: configuring tracing subscribers, stacking Tower middleware correctly (CORS, timeouts, panic recovery, rate limiting), creating standardized JSON error envelopes, and managing dependency injection lifecycles.
**Oxide removes that boilerplate and enforces correct conventions while keeping Axum-level performance.**
### The Killer Feature: Zero-Config Production APIs
Oxide ships with secure, production-tested defaults out of the box. You get automatic request logging, global panic recovery, standardized JSON success/error envelopes, and deterministic middleware ordering—all without writing a single line of configuration.
### Axum vs. Oxide: The Boilerplate Difference
**Raw Axum (30+ lines of exact middleware ordering & setup):**
```rust
// A typical production Axum setup
let app = Router::new()
.route("/api/users", get(list_users))
// Middleware order is critical and easy to get wrong!
.layer(CatchPanicLayer::new())
.layer(CorsLayer::permissive())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(TraceLayer::new_for_http())
.with_state(db_pool);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
```
**Oxide (5 lines, zero configuration for the same production readiness):**
```rust
// Everything above is handled automatically and correctly.
App::new()
.controller::<UserController>()
.run();
```
## Quickstart — Controller Style
```rust
use oxide_framework_core::{controller, App, AppState, ApiResponse, Json, Path};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct User { id: u64, name: String }
#[derive(Deserialize)]
struct CreateUser { name: String }
struct UserController;
#[controller("/api/users")]
impl UserController {
#[get("/")]
async fn list(&self) -> ApiResponse<Vec<User>> {
ApiResponse::ok(vec![User { id: 1, name: "Alice".into() }])
}
#[get("/{id}")]
async fn get_one(&self, Path(id): Path<u64>) -> ApiResponse<User> {
ApiResponse::ok(User { id, name: format!("User#{id}") })
}
#[post("/")]
async fn create(&self, Json(body): Json<CreateUser>) -> ApiResponse<User> {
ApiResponse::created(User { id: 42, name: body.name })
}
}
fn main() {
App::new()
.config("app.yaml")
.rate_limit(100, 60)
.cors_permissive()
.request_timeout(30)
.controller::<UserController>()
.run();
}
```
## Quickstart — Functional Style
```rust
use oxide_framework_core::{App, ApiResponse, Config};
use serde::Serialize;
#[derive(Serialize)]
struct Message { text: String }
async fn index(Config(cfg): Config) -> ApiResponse<Message> {
ApiResponse::ok(Message { text: format!("Hello from {}!", cfg.app_name) })
}
fn main() {
App::new()
.config("app.yaml")
.rate_limit(100, 60)
.cors_permissive()
.request_timeout(30)
.get("/", index)
.run();
}
```
That gives you:
- A running HTTP server on `127.0.0.1:3000`
- Configuration loaded from YAML + environment variables
- Shared state accessible in handlers via extractors
- Per-request logging (method, path, status, latency)
- Per-IP rate limiting (429 JSON on exceeded)
- CORS headers for cross-origin requests
- Request timeout enforcement (408 JSON on timeout)
- Graceful shutdown on Ctrl+C / SIGTERM
- Standardized JSON response envelopes
## CLI (`oxide`)
Scaffold apps and generate controllers without boilerplate:
```bash
cargo install --path oxide_cli # installs `oxide` on PATH
oxide new my-api --oxide path=../oxide_framework_core
cd my-api && cargo run
oxide generate controller Product --prefix /api/products
oxide generate route ProductController GET /featured
oxide run -- --release
```
From the Oxide repo, `oxide test` runs the full workspace test suite; `oxide bench` runs Criterion benchmarks plus the load-test example. See [docs/cli.md](docs/cli.md) for all commands and flags.
## Project Structure
```
Oxide/
├── Cargo.toml # Workspace root
├── app.yaml # Application config
│
├── oxide_framework_core/ # Framework library
│ ├── src/
│ │ ├── lib.rs # Public API exports
│ │ ├── app.rs # App builder + server lifecycle
│ │ ├── router.rs # OxideRouter, Method enum
│ │ ├── response.rs # ApiResponse, JSON envelopes
│ │ ├── config.rs # AppConfig, YAML + env loading
│ │ ├── state.rs # AppState, TypeMap (shared state)
│ │ ├── extract.rs # Config, Data<T> extractors
│ │ ├── middleware.rs # Request logger, state injection layer
│ │ └── logging.rs # tracing-subscriber init
│ └── examples/
│ └── hello.rs # Full working example
│
├── oxide_framework_macros/ # Proc-macro crate (#[controller], route attrs)
└── oxide_cli/ # `oxide` CLI — scaffold, generate, run, test, bench
```
## Public API at a Glance
| `App` | Builder for creating and running an Oxide application |
| `#[controller]` | Proc-macro: turns an `impl` block into a routable controller |
| `Controller` | Trait generated by `#[controller]` (also usable manually) |
| `OxideRouter` | Standalone router for modular route groups |
| `Method` | Enum: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` |
| `ApiResponse<T>` | Standardized JSON response with success/error envelopes |
| `AppConfig` | Configuration struct (YAML + env vars) |
| `AppState` | Shared state container (config + user extensions) |
| `Config` | Extractor for `AppConfig` in handlers |
| `Data<T>` | Extractor for user-provided state in handlers |
| `Inject<T>` | Alias for `Data<T>`, reads naturally in controller methods |
| `AuthConfig` / `App::auth` | HS256 JWT from `Authorization: Bearer` and/or a session cookie |
| `AuthClaims` | Decoded JWT subject + roles (in request extensions) |
| `Authenticated`, `OptionalAuth` | Extractors for logged-in / optional identity |
| `RequireRole<R>`, `RoleName` | Role guard (403 when role missing) |
| `encode_token` | Mint a JWT for login handlers / tests |
| `Json` | Re-export of `axum::Json` for request/response bodies |
| `Path` | Re-export of `axum::extract::Path` for path parameters |
| `StatusCode` | Re-export of `axum::http::StatusCode` |
## Documentation
- [Getting Started](docs/getting-started.md) — Setup, first app, running the server
- [Routing](docs/routing.md) — Methods, nesting, merging, path parameters
- [Responses](docs/responses.md) — ApiResponse, JSON envelopes, error handling
- [Configuration](docs/configuration.md) — YAML files, environment variables, defaults
- [State Management](docs/state.md) — Shared state, Config/Data extractors, thread safety
- [Middleware](docs/middleware.md) — Request logging, middleware architecture, custom middleware
- [Authentication](docs/auth.md) — JWT, session cookies, role guards
- [Architecture](docs/architecture.md) — Crate layout, data flow, design principles
- [CLI](docs/cli.md) — `oxide new`, `generate`, `run`, `test`, `bench`
## Dependencies
| `axum` 0.8 | HTTP routing and handler framework |
| `tokio` 1 | Async runtime |
| `tower` 0.5 | Middleware layer/service abstractions |
| `tower-http` 0.6 | CORS, panic recovery |
| `syn` 2 / `quote` 1 | Proc-macro parsing and code generation |
| `serde` / `serde_json` / `serde_yaml` | Serialization and config parsing |
| `tracing` / `tracing-subscriber` | Structured logging |
## Running the Example
```bash
cd oxide_framework_core
cargo run --example hello
```
Then:
```bash
curl http://127.0.0.1:3000/
# {"status":200,"data":{"text":"Hello from my-oxide-app!"}}
curl http://127.0.0.1:3000/stats
# {"status":200,"data":{"app_name":"my-oxide-app","total_users_created":0}}
curl http://127.0.0.1:3000/api/users
# {"status":200,"data":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]}
curl -X POST http://127.0.0.1:3000/api/users -H "Content-Type: application/json" -d '{"name":"Charlie"}'
# {"status":201,"data":{"id":1,"name":"Charlie"}}
curl http://127.0.0.1:3000/stats
# {"status":200,"data":{"app_name":"my-oxide-app","total_users_created":1}}
```
Server logs:
```
INFO oxide_framework_core::app: Oxide server started name=my-oxide-app address=127.0.0.1:3000
INFO oxide_framework_core::middleware: request completed method=GET path=/ status=200 latency_ms=0
INFO oxide_framework_core::middleware: request completed method=POST path=/api/users status=201 latency_ms=0
INFO oxide_framework_core::middleware: request completed method=GET path=/api/users/0 status=404 latency_ms=0
```
## License
MIT