Skip to content

Commit 794d34a

Browse files
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>
1 parent 61e02eb commit 794d34a

3 files changed

Lines changed: 89 additions & 94 deletions

File tree

docs/cookbook/src/getting_started/quickstart.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ cd my-api
1414

1515
This commands sets up a complete project structure with handling, models, and tests ready to go.
1616

17+
## The Code
18+
19+
Open `src/main.rs`. You'll see how simple it is:
20+
21+
```rust
22+
use rustapi_rs::prelude::*;
23+
24+
#[rustapi::get("/hello")]
25+
async fn hello() -> Json<String> {
26+
Json("Hello from RustAPI!".to_string())
27+
}
28+
29+
#[rustapi::main]
30+
async fn main() -> Result<()> {
31+
// Auto-discovery magic ✨
32+
RustApi::auto()
33+
.run("127.0.0.1:8080")
34+
.await
35+
}
36+
```
37+
1738
## Run the Server
1839

1940
Start your API server:
@@ -25,15 +46,15 @@ cargo run
2546
You should see output similar to:
2647

2748
```
28-
INFO 🚀 Server running at http://127.0.0.1:8080
29-
INFO 📚 API docs at http://127.0.0.1:8080/docs
49+
INFO rustapi: 🚀 Server running at http://127.0.0.1:8080
50+
INFO rustapi: 📚 API docs at http://127.0.0.1:8080/docs
3051
```
3152

3253
## Test It Out
3354

3455
Open your browser to [http://127.0.0.1:8080/docs](http://127.0.0.1:8080/docs).
3556

36-
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.
57+
You'll see the **Swagger UI** automatically generated from your code. Try out the endpoint directly from the browser!
3758

3859
## What Just Happened?
3960

docs/cookbook/src/recipes/crud_resource.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,24 @@ pub async fn create(Json(payload): Json<CreateUser>) -> impl IntoResponse {
3232
}
3333
```
3434

35-
Then register it in `main.rs`:
35+
Then in `main.rs`, simply use `RustApi::auto()`:
3636

3737
```rust
38-
RustApi::new()
39-
.mount(handlers::users::list)
40-
.mount(handlers::users::create)
38+
use rustapi_rs::prelude::*;
39+
40+
mod handlers; // Make sure the module is part of the compilation unit!
41+
42+
#[rustapi::main]
43+
async fn main() -> Result<()> {
44+
// RustAPI automatically discovers all routes decorated with macros
45+
RustApi::auto()
46+
.run("127.0.0.1:8080")
47+
.await
48+
}
4149
```
4250

4351
## Discussion
4452

45-
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.
53+
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.
54+
55+
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.
Lines changed: 50 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,117 @@
11
# JWT Authentication
22

3-
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.
3+
Authentication is critical for almost every API. RustAPI provides a built-in, production-ready JWT authentication system via the `jwt` feature.
44

55
## Dependencies
66

7-
Add `jsonwebtoken` and `serde` to your `Cargo.toml`:
7+
Enable the `jwt` feature in your `Cargo.toml`:
88

99
```toml
1010
[dependencies]
11-
jsonwebtoken = "9"
11+
rustapi-rs = { version = "0.1", features = ["jwt"] }
1212
serde = { version = "1", features = ["derive"] }
1313
```
1414

1515
## 1. Define Claims
1616

17-
The standard JWT claims. You can add custom fields here (like `role`).
17+
Define your custom claims struct. It must be serializable and deserializable.
1818

1919
```rust
2020
use serde::{Deserialize, Serialize};
2121

22-
#[derive(Debug, Serialize, Deserialize)]
22+
#[derive(Debug, Serialize, Deserialize, Clone)]
2323
pub struct Claims {
2424
pub sub: String, // Subject (User ID)
25-
pub exp: usize, // Expiration time
2625
pub role: String, // Custom claim: "admin", "user"
26+
// 'exp' is handled automatically by the framework if not present
2727
}
2828
```
2929

30-
## 2. Configuration State
30+
## 2. Shared State
3131

32-
Store your keys in the application state.
32+
To avoid hardcoding secrets in multiple places, we'll store our secret key in the application state.
3333

3434
```rust
35-
use std::sync::Arc;
36-
use jsonwebtoken::{EncodingKey, DecodingKey};
37-
3835
#[derive(Clone)]
39-
pub struct AuthState {
40-
pub encoder: EncodingKey,
41-
pub decoder: DecodingKey,
42-
}
43-
44-
impl AuthState {
45-
pub fn new(secret: &str) -> Self {
46-
Self {
47-
encoder: EncodingKey::from_secret(secret.as_bytes()),
48-
decoder: DecodingKey::from_secret(secret.as_bytes()),
49-
}
50-
}
36+
pub struct AppState {
37+
pub secret: String,
5138
}
5239
```
5340

54-
## 3. The `AuthUser` Extractor
41+
## 3. The Handlers
5542

56-
This is where the magic happens. We create a custom extractor that:
57-
1. Checks the `Authorization` header.
58-
2. Decodes the token.
59-
3. Validates expiration.
60-
4. Returns the claims or rejects the request.
43+
We use the `AuthUser<T>` extractor to protect routes, and `State<T>` to access the secret for signing tokens during login.
6144

6245
```rust
63-
use rustapi::prelude::*;
64-
use jsonwebtoken::{decode, Validation, Algorithm};
65-
66-
pub struct AuthUser(pub Claims);
67-
68-
#[async_trait]
69-
impl FromRequestParts<Arc<AuthState>> for AuthUser {
70-
type Rejection = (StatusCode, Json<serde_json::Value>);
71-
72-
async fn from_request_parts(
73-
parts: &mut Parts,
74-
state: &Arc<AuthState>
75-
) -> Result<Self, Self::Rejection> {
76-
// 1. Get header
77-
let auth_header = parts.headers.get("Authorization")
78-
.ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Missing token"}))))?;
79-
80-
let token = auth_header.to_str()
81-
.map_err(|_| (StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token format"}))))?
82-
.strip_prefix("Bearer ")
83-
.ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token type"}))))?;
84-
85-
// 2. Decode
86-
let token_data = decode::<Claims>(
87-
token,
88-
&state.decoder,
89-
&Validation::new(Algorithm::HS256)
90-
).map_err(|e| (StatusCode::UNAUTHORIZED, Json(json!({"error": e.to_string()}))))?;
91-
92-
Ok(AuthUser(token_data.claims))
93-
}
94-
}
95-
```
96-
97-
## 4. Usage in Handlers
46+
use rustapi_rs::prelude::*;
9847

99-
Now, securing an endpoint is as simple as adding an argument.
100-
101-
```rust
48+
#[rustapi::get("/profile")]
10249
async fn protected_profile(
103-
AuthUser(claims): AuthUser
50+
// This handler will only be called if a valid token is present
51+
AuthUser(claims): AuthUser<Claims>
10452
) -> Json<String> {
10553
Json(format!("Welcome back, {}! You are a {}.", claims.sub, claims.role))
10654
}
10755

108-
async fn login(State(state): State<Arc<AuthState>>) -> Json<String> {
56+
#[rustapi::post("/login")]
57+
async fn login(State(state): State<AppState>) -> Result<Json<String>> {
10958
// In a real app, validate credentials first!
11059
let claims = Claims {
11160
sub: "user_123".to_owned(),
11261
role: "admin".to_owned(),
113-
exp: 10000000000, // Future timestamp
11462
};
11563

116-
let token = jsonwebtoken::encode(
117-
&jsonwebtoken::Header::default(),
118-
&claims,
119-
&state.encoder
120-
).unwrap();
64+
// Create a token that expires in 1 hour (3600 seconds)
65+
// We use the secret from our shared state
66+
let token = create_token(&claims, &state.secret, 3600)?;
12167

122-
Json(token)
68+
Ok(Json(token))
12369
}
12470
```
12571

126-
## 5. Wiring it Up
72+
## 4. Wiring it Up
73+
74+
Register the `JwtLayer` and the state in your application.
12775

12876
```rust
12977
#[tokio::main]
130-
async fn main() {
131-
let auth_state = Arc::new(AuthState::new("my_secret_key"));
78+
async fn main() -> Result<()> {
79+
// In production, load this from an environment variable!
80+
let secret = "my_secret_key".to_string();
81+
82+
let state = AppState {
83+
secret: secret.clone(),
84+
};
13285

133-
let app = RustApi::new()
134-
.route("/login", post(login))
135-
.route("/profile", get(protected_profile))
136-
.with_state(auth_state); // Inject state
86+
// Configure JWT validation with the same secret
87+
let jwt_layer = JwtLayer::new(secret)
88+
.with_algorithm(jsonwebtoken::Algorithm::HS256);
13789

138-
RustApi::serve("127.0.0.1:3000", app).await.unwrap();
90+
RustApi::auto()
91+
.state(state) // Register the shared state
92+
.layer(jwt_layer) // Add the middleware
93+
.run("127.0.0.1:8080")
94+
.await
13995
}
14096
```
14197

14298
## Bonus: Role-Based Access Control (RBAC)
14399

144-
Since we have the `role` in our claims, we can enforce permissions easily.
100+
Since we have the `role` in our claims, we can enforce permissions easily within the handler:
145101

146102
```rust
147-
async fn admin_only(AuthUser(claims): AuthUser) -> Result<String, StatusCode> {
103+
#[rustapi::get("/admin")]
104+
async fn admin_only(AuthUser(claims): AuthUser<Claims>) -> Result<String, StatusCode> {
148105
if claims.role != "admin" {
149106
return Err(StatusCode::FORBIDDEN);
150107
}
151108
Ok("Sensitive Admin Data".to_string())
152109
}
153110
```
111+
112+
## How It Works
113+
114+
1. **`JwtLayer` Middleware**: Intercepts requests, looks for `Authorization: Bearer <token>`, validates the signature, and stores the decoded claims in the request extensions.
115+
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.
116+
117+
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.

0 commit comments

Comments
 (0)