From 794d34a47ef3d12a90fdaaf0aed3808ba42188e0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:37:05 +0000 Subject: [PATCH 1/4] docs: update cookbook recipes to match v0.1.233 API Updates the RustAPI cookbook recipes and quickstart guide to reflect the latest API patterns and best practices. Key changes: - `recipes/crud_resource.md`: Replaced deprecated `mount()` calls with `RustApi::auto()` for zero-config routing. - `recipes/jwt_auth.md`: Updated to use the built-in `jwt` feature from `rustapi-extras` instead of manual implementation. Added `AppState` example for secure secret management. - `getting_started/quickstart.md`: Added code snippets to clarify the `cargo rustapi new` generated code and `RustApi::auto()` usage. These changes ensure the documentation aligns with the current codebase features and "Zero Config" philosophy. Co-authored-by: Tuntii <121901995+Tuntii@users.noreply.github.com> --- .../src/getting_started/quickstart.md | 27 +++- docs/cookbook/src/recipes/crud_resource.md | 20 ++- docs/cookbook/src/recipes/jwt_auth.md | 136 +++++++----------- 3 files changed, 89 insertions(+), 94 deletions(-) diff --git a/docs/cookbook/src/getting_started/quickstart.md b/docs/cookbook/src/getting_started/quickstart.md index a6a31c3..be92f98 100644 --- a/docs/cookbook/src/getting_started/quickstart.md +++ b/docs/cookbook/src/getting_started/quickstart.md @@ -14,6 +14,27 @@ cd my-api This commands sets up a complete project structure with handling, models, and tests ready to go. +## The Code + +Open `src/main.rs`. You'll see how simple it is: + +```rust +use rustapi_rs::prelude::*; + +#[rustapi::get("/hello")] +async fn hello() -> Json { + Json("Hello from RustAPI!".to_string()) +} + +#[rustapi::main] +async fn main() -> Result<()> { + // Auto-discovery magic ✨ + RustApi::auto() + .run("127.0.0.1:8080") + .await +} +``` + ## Run the Server Start your API server: @@ -25,15 +46,15 @@ cargo run You should see output similar to: ``` -INFO 🚀 Server running at http://127.0.0.1:8080 -INFO 📚 API docs at http://127.0.0.1:8080/docs +INFO rustapi: 🚀 Server running at http://127.0.0.1:8080 +INFO rustapi: 📚 API docs at http://127.0.0.1:8080/docs ``` ## Test It Out Open your browser to [http://127.0.0.1:8080/docs](http://127.0.0.1:8080/docs). -You'll see the **Swagger UI** automatically generated from your code. Try out the `/health` endpoint or create a new Item in the `Items` API. +You'll see the **Swagger UI** automatically generated from your code. Try out the endpoint directly from the browser! ## What Just Happened? diff --git a/docs/cookbook/src/recipes/crud_resource.md b/docs/cookbook/src/recipes/crud_resource.md index fff9814..6b9a8a3 100644 --- a/docs/cookbook/src/recipes/crud_resource.md +++ b/docs/cookbook/src/recipes/crud_resource.md @@ -32,14 +32,24 @@ pub async fn create(Json(payload): Json) -> impl IntoResponse { } ``` -Then register it in `main.rs`: +Then in `main.rs`, simply use `RustApi::auto()`: ```rust -RustApi::new() - .mount(handlers::users::list) - .mount(handlers::users::create) +use rustapi_rs::prelude::*; + +mod handlers; // Make sure the module is part of the compilation unit! + +#[rustapi::main] +async fn main() -> Result<()> { + // RustAPI automatically discovers all routes decorated with macros + RustApi::auto() + .run("127.0.0.1:8080") + .await +} ``` ## Discussion -Using `#[rustapi::mount]` (if available) or manual routing keeps your `main.rs` clean. Organizing handlers by resource (domain-driven design) scales better than organizing by HTTP method. +RustAPI uses **distributed slices** (via `linkme`) to automatically register routes decorated with `#[rustapi::get]`, `#[rustapi::post]`, etc. This means you don't need to manually import or mount every single handler in your `main` function. + +Just ensure your handler modules are reachable (e.g., via `mod handlers;`), and the framework handles the rest. This encourages a clean, Domain-Driven Design (DDD) structure where resources are self-contained. diff --git a/docs/cookbook/src/recipes/jwt_auth.md b/docs/cookbook/src/recipes/jwt_auth.md index a563566..0d0c53e 100644 --- a/docs/cookbook/src/recipes/jwt_auth.md +++ b/docs/cookbook/src/recipes/jwt_auth.md @@ -1,153 +1,117 @@ # JWT Authentication -Authentication is critical for almost every API. This recipe demonstrates how to implement JSON Web Token (JWT) authentication using the `jsonwebtoken` crate and RustAPI's extractor pattern. +Authentication is critical for almost every API. RustAPI provides a built-in, production-ready JWT authentication system via the `jwt` feature. ## Dependencies -Add `jsonwebtoken` and `serde` to your `Cargo.toml`: +Enable the `jwt` feature in your `Cargo.toml`: ```toml [dependencies] -jsonwebtoken = "9" +rustapi-rs = { version = "0.1", features = ["jwt"] } serde = { version = "1", features = ["derive"] } ``` ## 1. Define Claims -The standard JWT claims. You can add custom fields here (like `role`). +Define your custom claims struct. It must be serializable and deserializable. ```rust use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { pub sub: String, // Subject (User ID) - pub exp: usize, // Expiration time pub role: String, // Custom claim: "admin", "user" + // 'exp' is handled automatically by the framework if not present } ``` -## 2. Configuration State +## 2. Shared State -Store your keys in the application state. +To avoid hardcoding secrets in multiple places, we'll store our secret key in the application state. ```rust -use std::sync::Arc; -use jsonwebtoken::{EncodingKey, DecodingKey}; - #[derive(Clone)] -pub struct AuthState { - pub encoder: EncodingKey, - pub decoder: DecodingKey, -} - -impl AuthState { - pub fn new(secret: &str) -> Self { - Self { - encoder: EncodingKey::from_secret(secret.as_bytes()), - decoder: DecodingKey::from_secret(secret.as_bytes()), - } - } +pub struct AppState { + pub secret: String, } ``` -## 3. The `AuthUser` Extractor +## 3. The Handlers -This is where the magic happens. We create a custom extractor that: -1. Checks the `Authorization` header. -2. Decodes the token. -3. Validates expiration. -4. Returns the claims or rejects the request. +We use the `AuthUser` extractor to protect routes, and `State` to access the secret for signing tokens during login. ```rust -use rustapi::prelude::*; -use jsonwebtoken::{decode, Validation, Algorithm}; - -pub struct AuthUser(pub Claims); - -#[async_trait] -impl FromRequestParts> for AuthUser { - type Rejection = (StatusCode, Json); - - async fn from_request_parts( - parts: &mut Parts, - state: &Arc - ) -> Result { - // 1. Get header - let auth_header = parts.headers.get("Authorization") - .ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Missing token"}))))?; - - let token = auth_header.to_str() - .map_err(|_| (StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token format"}))))? - .strip_prefix("Bearer ") - .ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token type"}))))?; - - // 2. Decode - let token_data = decode::( - token, - &state.decoder, - &Validation::new(Algorithm::HS256) - ).map_err(|e| (StatusCode::UNAUTHORIZED, Json(json!({"error": e.to_string()}))))?; - - Ok(AuthUser(token_data.claims)) - } -} -``` - -## 4. Usage in Handlers +use rustapi_rs::prelude::*; -Now, securing an endpoint is as simple as adding an argument. - -```rust +#[rustapi::get("/profile")] async fn protected_profile( - AuthUser(claims): AuthUser + // This handler will only be called if a valid token is present + AuthUser(claims): AuthUser ) -> Json { Json(format!("Welcome back, {}! You are a {}.", claims.sub, claims.role)) } -async fn login(State(state): State>) -> Json { +#[rustapi::post("/login")] +async fn login(State(state): State) -> Result> { // In a real app, validate credentials first! let claims = Claims { sub: "user_123".to_owned(), role: "admin".to_owned(), - exp: 10000000000, // Future timestamp }; - let token = jsonwebtoken::encode( - &jsonwebtoken::Header::default(), - &claims, - &state.encoder - ).unwrap(); + // Create a token that expires in 1 hour (3600 seconds) + // We use the secret from our shared state + let token = create_token(&claims, &state.secret, 3600)?; - Json(token) + Ok(Json(token)) } ``` -## 5. Wiring it Up +## 4. Wiring it Up + +Register the `JwtLayer` and the state in your application. ```rust #[tokio::main] -async fn main() { - let auth_state = Arc::new(AuthState::new("my_secret_key")); +async fn main() -> Result<()> { + // In production, load this from an environment variable! + let secret = "my_secret_key".to_string(); + + let state = AppState { + secret: secret.clone(), + }; - let app = RustApi::new() - .route("/login", post(login)) - .route("/profile", get(protected_profile)) - .with_state(auth_state); // Inject state + // Configure JWT validation with the same secret + let jwt_layer = JwtLayer::new(secret) + .with_algorithm(jsonwebtoken::Algorithm::HS256); - RustApi::serve("127.0.0.1:3000", app).await.unwrap(); + RustApi::auto() + .state(state) // Register the shared state + .layer(jwt_layer) // Add the middleware + .run("127.0.0.1:8080") + .await } ``` ## Bonus: Role-Based Access Control (RBAC) -Since we have the `role` in our claims, we can enforce permissions easily. +Since we have the `role` in our claims, we can enforce permissions easily within the handler: ```rust -async fn admin_only(AuthUser(claims): AuthUser) -> Result { +#[rustapi::get("/admin")] +async fn admin_only(AuthUser(claims): AuthUser) -> Result { if claims.role != "admin" { return Err(StatusCode::FORBIDDEN); } Ok("Sensitive Admin Data".to_string()) } ``` + +## How It Works + +1. **`JwtLayer` Middleware**: Intercepts requests, looks for `Authorization: Bearer `, validates the signature, and stores the decoded claims in the request extensions. +2. **`AuthUser` Extractor**: Retrieves the claims from the request extensions. If the middleware failed or didn't run, or if the token was missing/invalid, the extractor returns a `401 Unauthorized` error. + +This separation allows you to have some public routes (where `JwtLayer` might just pass through) and some protected routes (where `AuthUser` enforces presence). Note that `JwtLayer` by default does *not* reject requests without tokens; it just doesn't attach claims. The *extractor* does the rejection. From bbb58d91ba1a442aef78ec6e8f8bcdca5e0e353b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:47:42 +0000 Subject: [PATCH 2/4] Initial plan From 60f61a45ab91d454b13e7a562355457b9d67d1ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:49:19 +0000 Subject: [PATCH 3/4] docs: fix JWT auth recipe and quickstart based on review comments Co-authored-by: Tuntii <121901995+Tuntii@users.noreply.github.com> --- .../src/getting_started/quickstart.md | 2 +- docs/cookbook/src/recipes/jwt_auth.md | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/cookbook/src/getting_started/quickstart.md b/docs/cookbook/src/getting_started/quickstart.md index be92f98..df81d4d 100644 --- a/docs/cookbook/src/getting_started/quickstart.md +++ b/docs/cookbook/src/getting_started/quickstart.md @@ -12,7 +12,7 @@ cargo rustapi new my-api cd my-api ``` -This commands sets up a complete project structure with handling, models, and tests ready to go. +This command sets up a complete project structure with handling, models, and tests ready to go. ## The Code diff --git a/docs/cookbook/src/recipes/jwt_auth.md b/docs/cookbook/src/recipes/jwt_auth.md index 0d0c53e..63e83c6 100644 --- a/docs/cookbook/src/recipes/jwt_auth.md +++ b/docs/cookbook/src/recipes/jwt_auth.md @@ -21,9 +21,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { - pub sub: String, // Subject (User ID) - pub role: String, // Custom claim: "admin", "user" - // 'exp' is handled automatically by the framework if not present + pub sub: String, // Subject (User ID) + pub role: String, // Custom claim: "admin", "user" + pub exp: usize, // Required for JWT expiration validation } ``` @@ -56,14 +56,21 @@ async fn protected_profile( #[rustapi::post("/login")] async fn login(State(state): State) -> Result> { // In a real app, validate credentials first! + use std::time::{SystemTime, UNIX_EPOCH}; + + let expiration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + 3600; // Token expires in 1 hour (3600 seconds) + let claims = Claims { sub: "user_123".to_owned(), role: "admin".to_owned(), + exp: expiration as usize, }; - // Create a token that expires in 1 hour (3600 seconds) // We use the secret from our shared state - let token = create_token(&claims, &state.secret, 3600)?; + let token = create_token(&claims, &state.secret)?; Ok(Json(token)) } @@ -74,7 +81,7 @@ async fn login(State(state): State) -> Result> { Register the `JwtLayer` and the state in your application. ```rust -#[tokio::main] +#[rustapi::main] async fn main() -> Result<()> { // In production, load this from an environment variable! let secret = "my_secret_key".to_string(); @@ -84,8 +91,7 @@ async fn main() -> Result<()> { }; // Configure JWT validation with the same secret - let jwt_layer = JwtLayer::new(secret) - .with_algorithm(jsonwebtoken::Algorithm::HS256); + let jwt_layer = JwtLayer::::new(secret); RustApi::auto() .state(state) // Register the shared state From 61d5b9f98b8d8c634dc3b3561a5f03735cd619b0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:50:12 +0000 Subject: [PATCH 4/4] docs: incorporate review feedback for cookbook recipes Updates the cookbook documentation based on PR review feedback: - `recipes/jwt_auth.md`: - Added `exp` field to `Claims` struct as it is required for validation. - Updated `create_token` usage to match the correct 2-argument signature. - Manually calculated `exp` time in the login handler. - Switched from `#[tokio::main]` to `#[rustapi::main]` for consistency. - Removed incorrect `.with_algorithm()` call on `JwtLayer` (HS256 is default). - `getting_started/quickstart.md`: Fixed a typo. Co-authored-by: Tuntii <121901995+Tuntii@users.noreply.github.com> --- .../src/getting_started/quickstart.md | 2 +- docs/cookbook/src/recipes/jwt_auth.md | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/cookbook/src/getting_started/quickstart.md b/docs/cookbook/src/getting_started/quickstart.md index be92f98..df81d4d 100644 --- a/docs/cookbook/src/getting_started/quickstart.md +++ b/docs/cookbook/src/getting_started/quickstart.md @@ -12,7 +12,7 @@ cargo rustapi new my-api cd my-api ``` -This commands sets up a complete project structure with handling, models, and tests ready to go. +This command sets up a complete project structure with handling, models, and tests ready to go. ## The Code diff --git a/docs/cookbook/src/recipes/jwt_auth.md b/docs/cookbook/src/recipes/jwt_auth.md index 0d0c53e..86be87e 100644 --- a/docs/cookbook/src/recipes/jwt_auth.md +++ b/docs/cookbook/src/recipes/jwt_auth.md @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; pub struct Claims { pub sub: String, // Subject (User ID) pub role: String, // Custom claim: "admin", "user" - // 'exp' is handled automatically by the framework if not present + pub exp: usize, // Expiration time (required for validation) } ``` @@ -44,6 +44,7 @@ We use the `AuthUser` extractor to protect routes, and `State` to access t ```rust use rustapi_rs::prelude::*; +use std::time::{SystemTime, UNIX_EPOCH}; #[rustapi::get("/profile")] async fn protected_profile( @@ -56,14 +57,21 @@ async fn protected_profile( #[rustapi::post("/login")] async fn login(State(state): State) -> Result> { // In a real app, validate credentials first! + + // Calculate expiration (1 hour from now) + let exp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as usize + 3600; + let claims = Claims { sub: "user_123".to_owned(), role: "admin".to_owned(), + exp, }; - // Create a token that expires in 1 hour (3600 seconds) - // We use the secret from our shared state - let token = create_token(&claims, &state.secret, 3600)?; + // Create the token using the secret from our shared state + let token = create_token(&claims, &state.secret)?; Ok(Json(token)) } @@ -74,7 +82,7 @@ async fn login(State(state): State) -> Result> { Register the `JwtLayer` and the state in your application. ```rust -#[tokio::main] +#[rustapi::main] async fn main() -> Result<()> { // In production, load this from an environment variable! let secret = "my_secret_key".to_string(); @@ -84,8 +92,7 @@ async fn main() -> Result<()> { }; // Configure JWT validation with the same secret - let jwt_layer = JwtLayer::new(secret) - .with_algorithm(jsonwebtoken::Algorithm::HS256); + let jwt_layer = JwtLayer::new(secret); RustApi::auto() .state(state) // Register the shared state