Skip to content

Commit 405a366

Browse files
authored
Merge pull request #80 from Tuntii/docs-update-cookbook-v0.1.233-1962074576845807621
docs: update cookbook recipes to match v0.1.233 API
2 parents 61e02eb + 56dcb43 commit 405a366

3 files changed

Lines changed: 100 additions & 98 deletions

File tree

docs/cookbook/src/getting_started/quickstart.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,28 @@ cargo rustapi new my-api
1212
cd my-api
1313
```
1414

15-
This commands sets up a complete project structure with handling, models, and tests ready to go.
15+
This command sets up a complete project structure with handling, models, and tests ready to go.
16+
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+
```
1637

1738
## Run the Server
1839

@@ -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: 60 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,124 @@
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 {
24-
pub sub: String, // Subject (User ID)
25-
pub exp: usize, // Expiration time
26-
pub role: String, // Custom claim: "admin", "user"
24+
pub sub: String, // Subject (User ID)
25+
pub role: String, // Custom claim: "admin", "user"
26+
pub exp: usize, // Required for JWT expiration validation
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::*;
47+
use std::time::{SystemTime, UNIX_EPOCH};
9848

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

108-
async fn login(State(state): State<Arc<AuthState>>) -> Json<String> {
57+
#[rustapi::post("/login")]
58+
async fn login(State(state): State<AppState>) -> Result<Json<String>> {
10959
// In a real app, validate credentials first!
60+
use std::time::{SystemTime, UNIX_EPOCH};
61+
62+
let expiration = SystemTime::now()
63+
.duration_since(UNIX_EPOCH)
64+
.unwrap()
65+
.as_secs() + 3600; // Token expires in 1 hour (3600 seconds)
66+
11067
let claims = Claims {
11168
sub: "user_123".to_owned(),
11269
role: "admin".to_owned(),
113-
exp: 10000000000, // Future timestamp
70+
exp: expiration as usize,
11471
};
11572

116-
let token = jsonwebtoken::encode(
117-
&jsonwebtoken::Header::default(),
118-
&claims,
119-
&state.encoder
120-
).unwrap();
73+
// We use the secret from our shared state
74+
let token = create_token(&claims, &state.secret)?;
12175

122-
Json(token)
76+
Ok(Json(token))
12377
}
12478
```
12579

126-
## 5. Wiring it Up
80+
## 4. Wiring it Up
81+
82+
Register the `JwtLayer` and the state in your application.
12783

12884
```rust
129-
#[tokio::main]
130-
async fn main() {
131-
let auth_state = Arc::new(AuthState::new("my_secret_key"));
85+
#[rustapi::main]
86+
async fn main() -> Result<()> {
87+
// In production, load this from an environment variable!
88+
let secret = "my_secret_key".to_string();
89+
90+
let state = AppState {
91+
secret: secret.clone(),
92+
};
13293

133-
let app = RustApi::new()
134-
.route("/login", post(login))
135-
.route("/profile", get(protected_profile))
136-
.with_state(auth_state); // Inject state
94+
// Configure JWT validation with the same secret
95+
let jwt_layer = JwtLayer::<Claims>::new(secret);
13796

138-
RustApi::serve("127.0.0.1:3000", app).await.unwrap();
97+
RustApi::auto()
98+
.state(state) // Register the shared state
99+
.layer(jwt_layer) // Add the middleware
100+
.run("127.0.0.1:8080")
101+
.await
139102
}
140103
```
141104

142105
## Bonus: Role-Based Access Control (RBAC)
143106

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

146109
```rust
147-
async fn admin_only(AuthUser(claims): AuthUser) -> Result<String, StatusCode> {
110+
#[rustapi::get("/admin")]
111+
async fn admin_only(AuthUser(claims): AuthUser<Claims>) -> Result<String, StatusCode> {
148112
if claims.role != "admin" {
149113
return Err(StatusCode::FORBIDDEN);
150114
}
151115
Ok("Sensitive Admin Data".to_string())
152116
}
153117
```
118+
119+
## How It Works
120+
121+
1. **`JwtLayer` Middleware**: Intercepts requests, looks for `Authorization: Bearer <token>`, validates the signature, and stores the decoded claims in the request extensions.
122+
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.
123+
124+
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)