pub struct AppBuilder<S = ()>where
S: StateRegistry,{ /* private fields */ }Expand description
Builder for constructing an App.
Use this to configure routes, middleware, and shared state before building the final application.
§Example
let app = App::builder()
.config(AppConfig::new().name("My API"))
.state(DatabasePool::new())
.middleware(LoggingMiddleware::new())
.on_startup(|| {
println!("Server starting...");
Ok(())
})
.on_shutdown(|| {
println!("Server stopping...");
})
.route("/", Method::Get, index_handler)
.route("/items", Method::Get, list_items)
.route("/items", Method::Post, create_item)
.route("/items/{id}", Method::Get, get_item)
.build();§Type-Safe State
The AppBuilder uses a type-state pattern to track registered state types
at compile time. The generic parameter S represents the type-level set of
registered state types.
When you call .with_state::<T>(value), the builder’s type changes from
AppBuilder<S> to AppBuilder<(T, S)>, recording that T is now available.
Handlers that use State<T> extractors can optionally be constrained to
require S: HasState<T>, ensuring the state is registered at compile time.
// Type changes: AppBuilder<()> -> AppBuilder<(DbPool, ())> -> AppBuilder<(Config, (DbPool, ()))>
let app = App::builder()
.with_state(DbPool::new()) // Now has DbPool
.with_state(Config::default()) // Now has DbPool + Config
.build();Implementations§
Source§impl AppBuilder
impl AppBuilder
Sourcepub fn new() -> AppBuilder
pub fn new() -> AppBuilder
Creates a new application builder with no registered state.
Source§impl<S> AppBuilder<S>where
S: StateRegistry,
impl<S> AppBuilder<S>where
S: StateRegistry,
Sourcepub fn config(self, config: AppConfig) -> AppBuilder<S>
pub fn config(self, config: AppConfig) -> AppBuilder<S>
Sets the application configuration.
Examples found in repository?
34fn main() {
35 println!("Getting Started Guide - Code Validation\n");
36
37 // === Basic App Example ===
38 println!("1. Basic app with two routes:");
39 let app = App::builder()
40 .get("/", hello)
41 .get("/health", health)
42 .build();
43
44 println!(" Routes: {}", app.route_count());
45 let client = TestClient::new(app);
46
47 let response = client.get("/").send();
48 println!(
49 " GET / -> {} ({})",
50 response.status().as_u16(),
51 response.text()
52 );
53 if !check_eq(response.status().as_u16(), 200, "GET / should return 200") {
54 return;
55 }
56 if !check_eq(response.text(), "Hello, World!", "GET / should return body") {
57 return;
58 }
59
60 let response = client.get("/health").send();
61 println!(
62 " GET /health -> {} ({})",
63 response.status().as_u16(),
64 response.text()
65 );
66 if !check_eq(
67 response.status().as_u16(),
68 200,
69 "GET /health should return 200",
70 ) {
71 return;
72 }
73
74 // === App with Middleware ===
75 println!("\n2. App with middleware:");
76 let app = App::builder()
77 .middleware(RequestIdMiddleware::new())
78 .middleware(SecurityHeaders::new())
79 .get("/", hello)
80 .build();
81
82 let client = TestClient::new(app);
83 let response = client.get("/").send();
84 println!(" GET / -> {}", response.status().as_u16());
85 if !check_eq(
86 response.status().as_u16(),
87 200,
88 "GET / with middleware should return 200",
89 ) {
90 return;
91 }
92
93 // === App with Configuration ===
94 println!("\n3. App with configuration:");
95 let config = AppConfig::new()
96 .name("My API")
97 .version("1.0.0")
98 .debug(true)
99 .max_body_size(10 * 1024 * 1024)
100 .request_timeout_ms(30_000);
101
102 let app = App::builder().config(config).get("/", hello).build();
103
104 println!(" App name: {}", app.config().name);
105 println!(" Version: {}", app.config().version);
106 if !check_eq(
107 app.config().name.as_str(),
108 "My API",
109 "Config name should match",
110 ) {
111 return;
112 }
113 if !check_eq(
114 app.config().version.as_str(),
115 "1.0.0",
116 "Config version should match",
117 ) {
118 return;
119 }
120
121 // === 404 for unknown routes ===
122 println!("\n4. 404 for unknown routes:");
123 let app = App::builder().get("/", hello).build();
124
125 let client = TestClient::new(app);
126 let response = client.get("/nonexistent").send();
127 println!(" GET /nonexistent -> {}", response.status().as_u16());
128 if !check_eq(
129 response.status().as_u16(),
130 404,
131 "Unknown routes should return 404",
132 ) {
133 return;
134 }
135
136 println!("\nAll getting started examples validated successfully!");
137}Sourcepub fn route<H, Fut>(
self,
path: impl Into<String>,
method: Method,
handler: H,
) -> AppBuilder<S>
pub fn route<H, Fut>( self, path: impl Into<String>, method: Method, handler: H, ) -> AppBuilder<S>
Adds a route to the application.
Routes are matched in the order they are added.
Sourcepub fn get<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
pub fn get<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
Adds a GET route.
Examples found in repository?
61fn main() {
62 println!("fastapi_rust Hello World Example");
63 println!("================================\n");
64
65 // Build the application
66 //
67 // App::builder() creates a new application builder that lets you:
68 // - Add routes for different HTTP methods
69 // - Configure middleware
70 // - Set application state
71 // - Define exception handlers
72 let app = App::builder()
73 // Register a GET handler for the root path "/"
74 //
75 // This is equivalent to:
76 // @app.get("/")
77 // def hello():
78 // return "Hello, World!"
79 // in Python FastAPI
80 .get("/", hello_handler)
81 // Build the final immutable App
82 .build();
83
84 println!("App created with {} route(s)\n", app.route_count());
85
86 // Create a test client to make requests to our app
87 //
88 // TestClient wraps any Handler (including App) and provides
89 // a convenient API for making HTTP requests in tests.
90 let client = TestClient::new(app);
91
92 // Make a GET request to "/"
93 println!("Making request: GET /");
94 let response = client.get("/").send();
95
96 // Check the response
97 println!(
98 "GET / -> {} {}",
99 response.status().as_u16(),
100 response.status().canonical_reason()
101 );
102 println!("Response: {}\n", response.text());
103
104 // Verify success
105 if !check_eq(response.status().as_u16(), 200, "GET / should return 200") {
106 return;
107 }
108 if !check_eq(response.text(), "Hello, World!", "GET / should return body") {
109 return;
110 }
111
112 // Try a path that doesn't exist - should get 404
113 println!("Making request: GET /not-found");
114 let response = client.get("/not-found").send();
115 println!(
116 "GET /not-found -> {} {}",
117 response.status().as_u16(),
118 response.status().canonical_reason()
119 );
120 if !check_eq(
121 response.status().as_u16(),
122 404,
123 "Unknown routes should return 404",
124 ) {
125 return;
126 }
127
128 println!("\nAll assertions passed!");
129}More examples
34fn main() {
35 println!("Getting Started Guide - Code Validation\n");
36
37 // === Basic App Example ===
38 println!("1. Basic app with two routes:");
39 let app = App::builder()
40 .get("/", hello)
41 .get("/health", health)
42 .build();
43
44 println!(" Routes: {}", app.route_count());
45 let client = TestClient::new(app);
46
47 let response = client.get("/").send();
48 println!(
49 " GET / -> {} ({})",
50 response.status().as_u16(),
51 response.text()
52 );
53 if !check_eq(response.status().as_u16(), 200, "GET / should return 200") {
54 return;
55 }
56 if !check_eq(response.text(), "Hello, World!", "GET / should return body") {
57 return;
58 }
59
60 let response = client.get("/health").send();
61 println!(
62 " GET /health -> {} ({})",
63 response.status().as_u16(),
64 response.text()
65 );
66 if !check_eq(
67 response.status().as_u16(),
68 200,
69 "GET /health should return 200",
70 ) {
71 return;
72 }
73
74 // === App with Middleware ===
75 println!("\n2. App with middleware:");
76 let app = App::builder()
77 .middleware(RequestIdMiddleware::new())
78 .middleware(SecurityHeaders::new())
79 .get("/", hello)
80 .build();
81
82 let client = TestClient::new(app);
83 let response = client.get("/").send();
84 println!(" GET / -> {}", response.status().as_u16());
85 if !check_eq(
86 response.status().as_u16(),
87 200,
88 "GET / with middleware should return 200",
89 ) {
90 return;
91 }
92
93 // === App with Configuration ===
94 println!("\n3. App with configuration:");
95 let config = AppConfig::new()
96 .name("My API")
97 .version("1.0.0")
98 .debug(true)
99 .max_body_size(10 * 1024 * 1024)
100 .request_timeout_ms(30_000);
101
102 let app = App::builder().config(config).get("/", hello).build();
103
104 println!(" App name: {}", app.config().name);
105 println!(" Version: {}", app.config().version);
106 if !check_eq(
107 app.config().name.as_str(),
108 "My API",
109 "Config name should match",
110 ) {
111 return;
112 }
113 if !check_eq(
114 app.config().version.as_str(),
115 "1.0.0",
116 "Config version should match",
117 ) {
118 return;
119 }
120
121 // === 404 for unknown routes ===
122 println!("\n4. 404 for unknown routes:");
123 let app = App::builder().get("/", hello).build();
124
125 let client = TestClient::new(app);
126 let response = client.get("/nonexistent").send();
127 println!(" GET /nonexistent -> {}", response.status().as_u16());
128 if !check_eq(
129 response.status().as_u16(),
130 404,
131 "Unknown routes should return 404",
132 ) {
133 return;
134 }
135
136 println!("\nAll getting started examples validated successfully!");
137}271fn main() {
272 // Initialize the global store
273 let mut guard = STORE
274 .lock()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 *guard = Some(UserDb {
277 users: HashMap::new(),
278 next_id: 1,
279 });
280
281 println!("fastapi_rust CRUD API Example");
282 println!("=============================\n");
283
284 let app = App::builder()
285 .post("/users", create_user)
286 .get("/users", list_users)
287 .get("/users/{id}", get_user)
288 .put("/users/{id}", update_user)
289 .delete("/users/{id}", delete_user)
290 .build();
291
292 println!("App created with {} route(s)\n", app.route_count());
293 let client = TestClient::new(app);
294
295 // 1. Create users
296 println!("1. Create users");
297 let resp = client
298 .post("/users")
299 .header("content-type", "application/json")
300 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
301 .send();
302 println!(
303 " POST /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
308 return;
309 }
310
311 let resp = client
312 .post("/users")
313 .header("content-type", "application/json")
314 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
315 .send();
316 println!(
317 " POST /users -> {} {}",
318 resp.status().as_u16(),
319 resp.text()
320 );
321 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
322 return;
323 }
324
325 // 2. List users
326 println!("\n2. List all users");
327 let resp = client.get("/users").send();
328 println!(
329 " GET /users -> {} {}",
330 resp.status().as_u16(),
331 resp.text()
332 );
333 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
334 return;
335 }
336
337 // 3. Get user by ID
338 println!("\n3. Get user by ID");
339 let resp = client.get("/users/1").send();
340 println!(
341 " GET /users/1 -> {} {}",
342 resp.status().as_u16(),
343 resp.text()
344 );
345 if !check_eq(resp.status().as_u16(), 200, "Get user should return 200") {
346 return;
347 }
348
349 // 4. Get nonexistent user
350 println!("\n4. Get nonexistent user");
351 let resp = client.get("/users/999").send();
352 println!(
353 " GET /users/999 -> {} {}",
354 resp.status().as_u16(),
355 resp.text()
356 );
357 if !check_eq(
358 resp.status().as_u16(),
359 404,
360 "Missing user should return 404",
361 ) {
362 return;
363 }
364
365 // 5. Update user
366 println!("\n5. Update user");
367 let resp = client
368 .put("/users/1")
369 .header("content-type", "application/json")
370 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
371 .send();
372 println!(
373 " PUT /users/1 -> {} {}",
374 resp.status().as_u16(),
375 resp.text()
376 );
377 if !check_eq(resp.status().as_u16(), 200, "Update user should return 200") {
378 return;
379 }
380
381 // 6. Validation: empty name
382 println!("\n6. Validation error (empty name)");
383 let resp = client
384 .post("/users")
385 .header("content-type", "application/json")
386 .body(r#"{"name": "", "email": "bad@example.com"}"#)
387 .send();
388 println!(
389 " POST /users -> {} {}",
390 resp.status().as_u16(),
391 resp.text()
392 );
393 if !check_eq(resp.status().as_u16(), 400, "Empty name should return 400") {
394 return;
395 }
396
397 // 7. Validation: invalid email
398 println!("\n7. Validation error (invalid email)");
399 let resp = client
400 .post("/users")
401 .header("content-type", "application/json")
402 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
403 .send();
404 println!(
405 " POST /users -> {} {}",
406 resp.status().as_u16(),
407 resp.text()
408 );
409 if !check_eq(
410 resp.status().as_u16(),
411 400,
412 "Invalid email should return 400",
413 ) {
414 return;
415 }
416
417 // 8. Wrong Content-Type
418 println!("\n8. Wrong Content-Type");
419 let resp = client
420 .post("/users")
421 .header("content-type", "text/plain")
422 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
423 .send();
424 println!(
425 " POST /users -> {} {}",
426 resp.status().as_u16(),
427 resp.text()
428 );
429 if !check_eq(
430 resp.status().as_u16(),
431 415,
432 "Wrong Content-Type should return 415",
433 ) {
434 return;
435 }
436
437 // 9. Delete user
438 println!("\n9. Delete user");
439 let resp = client.delete("/users/2").send();
440 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
441 if !check_eq(resp.status().as_u16(), 204, "Delete user should return 204") {
442 return;
443 }
444
445 // 10. Verify deletion
446 println!("\n10. Verify deletion");
447 let resp = client.get("/users/2").send();
448 println!(
449 " GET /users/2 -> {} {}",
450 resp.status().as_u16(),
451 resp.text()
452 );
453 if !check_eq(
454 resp.status().as_u16(),
455 404,
456 "Deleted user should return 404",
457 ) {
458 return;
459 }
460
461 let resp = client.get("/users").send();
462 println!(
463 " GET /users -> {} {}",
464 resp.status().as_u16(),
465 resp.text()
466 );
467 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
468 return;
469 }
470
471 println!("\nAll CRUD operations passed!");
472}267fn main() {
268 println!("fastapi_rust Authentication Example");
269 println!("====================================\n");
270
271 // Build the application with public and protected routes
272 let app = App::builder()
273 // Public endpoints - accessible to everyone
274 .get("/public", public_handler)
275 // Login endpoint - returns a token
276 .post("/login", login_handler)
277 // Protected endpoint - requires valid bearer token
278 .get("/protected", protected_handler)
279 .build();
280
281 println!("App created with {} route(s)\n", app.route_count());
282
283 // Create a test client
284 let client = TestClient::new(app);
285
286 // =========================================================================
287 // Test 1: Public endpoint - no auth required
288 // =========================================================================
289 println!("1. Public endpoint - no auth required");
290 let response = client.get("/public").send();
291 println!(
292 " GET /public -> {} {}",
293 response.status().as_u16(),
294 response.status().canonical_reason()
295 );
296 if !check_eq(
297 response.status().as_u16(),
298 200,
299 "GET /public should return 200",
300 ) {
301 return;
302 }
303 if !check(
304 response.text().contains("public endpoint"),
305 "GET /public should include the public endpoint body",
306 ) {
307 return;
308 }
309
310 // =========================================================================
311 // Test 2: Protected endpoint - without token (should get 401)
312 // =========================================================================
313 println!("\n2. Protected endpoint - without token");
314 let response = client.get("/protected").send();
315 println!(
316 " GET /protected -> {} {}",
317 response.status().as_u16(),
318 response.status().canonical_reason()
319 );
320 if !check_eq(
321 response.status().as_u16(),
322 401,
323 "Protected endpoint should return 401 without token",
324 ) {
325 return;
326 }
327
328 // Check for WWW-Authenticate header
329 let has_www_auth = response
330 .headers()
331 .iter()
332 .any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
333 if !check(
334 has_www_auth,
335 "401 response should include WWW-Authenticate: Bearer header",
336 ) {
337 return;
338 }
339
340 // =========================================================================
341 // Test 3: Login endpoint - get a token
342 // =========================================================================
343 println!("\n3. Login endpoint - get a token");
344 let response = client
345 .post("/login")
346 .header("content-type", "application/json")
347 .body(r#"{"username":"test","password":"test123"}"#)
348 .send();
349 println!(
350 " POST /login -> {} {}",
351 response.status().as_u16(),
352 response.status().canonical_reason()
353 );
354 if !check_eq(
355 response.status().as_u16(),
356 200,
357 "POST /login should return 200",
358 ) {
359 return;
360 }
361
362 // Parse the response to get the token
363 let body_text = response.text();
364 let body: serde_json::Value = match serde_json::from_str(body_text) {
365 Ok(body) => body,
366 Err(err) => {
367 eprintln!("Failed to parse login response JSON: {err}");
368 return;
369 }
370 };
371 let Some(bearer_value) = body.get("access_token").and_then(|value| value.as_str()) else {
372 eprintln!("Login response missing access_token");
373 return;
374 };
375 println!(" Bearer value: {bearer_value}");
376 if !check_eq(
377 bearer_value,
378 DEMO_BEARER_VALUE,
379 "Login should return the expected bearer value",
380 ) {
381 return;
382 }
383
384 // =========================================================================
385 // Test 4: Protected endpoint - with valid token (should get 200)
386 // =========================================================================
387 println!("\n4. Protected endpoint - with valid token");
388 let response = client
389 .get("/protected")
390 .header("authorization", format!("Bearer {DEMO_BEARER_VALUE}"))
391 .send();
392 println!(
393 " GET /protected (Authorization: Bearer {}) -> {} {}",
394 DEMO_BEARER_VALUE,
395 response.status().as_u16(),
396 response.status().canonical_reason()
397 );
398 if !check_eq(
399 response.status().as_u16(),
400 200,
401 "Protected endpoint should return 200 with valid token",
402 ) {
403 return;
404 }
405 if !check(
406 response.text().contains("protected resource"),
407 "Protected endpoint should include protected resource body",
408 ) {
409 return;
410 }
411
412 // =========================================================================
413 // Test 5: Protected endpoint - with invalid token (should get 403)
414 // =========================================================================
415 println!("\n5. Protected endpoint - with invalid token");
416 let response = client
417 .get("/protected")
418 .header("authorization", "Bearer wrong_token")
419 .send();
420 println!(
421 " GET /protected (Authorization: Bearer wrong_token) -> {} {}",
422 response.status().as_u16(),
423 response.status().canonical_reason()
424 );
425 if !check_eq(
426 response.status().as_u16(),
427 403,
428 "Protected endpoint should return 403 with invalid token",
429 ) {
430 return;
431 }
432
433 // =========================================================================
434 // Test 6: Protected endpoint - with wrong auth scheme (should get 401)
435 // =========================================================================
436 println!("\n6. Protected endpoint - with wrong auth scheme");
437 let response = client
438 .get("/protected")
439 .header("authorization", "Basic dXNlcjpwYXNz")
440 .send();
441 println!(
442 " GET /protected (Authorization: Basic ...) -> {} {}",
443 response.status().as_u16(),
444 response.status().canonical_reason()
445 );
446 if !check_eq(
447 response.status().as_u16(),
448 401,
449 "Protected endpoint should return 401 with wrong auth scheme",
450 ) {
451 return;
452 }
453
454 // =========================================================================
455 // Test 7: Login with wrong Content-Type (should get 415)
456 // =========================================================================
457 println!("\n7. Login with wrong Content-Type");
458 let response = client
459 .post("/login")
460 .header("content-type", "text/plain")
461 .body("demo=true")
462 .send();
463 println!(
464 " POST /login (Content-Type: text/plain) -> {} {}",
465 response.status().as_u16(),
466 response.status().canonical_reason()
467 );
468 if !check_eq(
469 response.status().as_u16(),
470 415,
471 "Login should return 415 with wrong Content-Type",
472 ) {
473 return;
474 }
475
476 // =========================================================================
477 // Test 8: Token case sensitivity (lowercase 'bearer')
478 // =========================================================================
479 println!("\n8. Token case sensitivity (lowercase 'bearer')");
480 let response = client
481 .get("/protected")
482 .header("authorization", format!("bearer {DEMO_BEARER_VALUE}"))
483 .send();
484 println!(
485 " GET /protected (Authorization: bearer {}) -> {} {}",
486 DEMO_BEARER_VALUE,
487 response.status().as_u16(),
488 response.status().canonical_reason()
489 );
490 if !check_eq(
491 response.status().as_u16(),
492 200,
493 "Bearer scheme should be case-insensitive (lowercase accepted)",
494 ) {
495 return;
496 }
497
498 println!("\nAll authentication tests passed!");
499}Sourcepub fn post<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
pub fn post<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
Adds a POST route.
Examples found in repository?
271fn main() {
272 // Initialize the global store
273 let mut guard = STORE
274 .lock()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 *guard = Some(UserDb {
277 users: HashMap::new(),
278 next_id: 1,
279 });
280
281 println!("fastapi_rust CRUD API Example");
282 println!("=============================\n");
283
284 let app = App::builder()
285 .post("/users", create_user)
286 .get("/users", list_users)
287 .get("/users/{id}", get_user)
288 .put("/users/{id}", update_user)
289 .delete("/users/{id}", delete_user)
290 .build();
291
292 println!("App created with {} route(s)\n", app.route_count());
293 let client = TestClient::new(app);
294
295 // 1. Create users
296 println!("1. Create users");
297 let resp = client
298 .post("/users")
299 .header("content-type", "application/json")
300 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
301 .send();
302 println!(
303 " POST /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
308 return;
309 }
310
311 let resp = client
312 .post("/users")
313 .header("content-type", "application/json")
314 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
315 .send();
316 println!(
317 " POST /users -> {} {}",
318 resp.status().as_u16(),
319 resp.text()
320 );
321 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
322 return;
323 }
324
325 // 2. List users
326 println!("\n2. List all users");
327 let resp = client.get("/users").send();
328 println!(
329 " GET /users -> {} {}",
330 resp.status().as_u16(),
331 resp.text()
332 );
333 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
334 return;
335 }
336
337 // 3. Get user by ID
338 println!("\n3. Get user by ID");
339 let resp = client.get("/users/1").send();
340 println!(
341 " GET /users/1 -> {} {}",
342 resp.status().as_u16(),
343 resp.text()
344 );
345 if !check_eq(resp.status().as_u16(), 200, "Get user should return 200") {
346 return;
347 }
348
349 // 4. Get nonexistent user
350 println!("\n4. Get nonexistent user");
351 let resp = client.get("/users/999").send();
352 println!(
353 " GET /users/999 -> {} {}",
354 resp.status().as_u16(),
355 resp.text()
356 );
357 if !check_eq(
358 resp.status().as_u16(),
359 404,
360 "Missing user should return 404",
361 ) {
362 return;
363 }
364
365 // 5. Update user
366 println!("\n5. Update user");
367 let resp = client
368 .put("/users/1")
369 .header("content-type", "application/json")
370 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
371 .send();
372 println!(
373 " PUT /users/1 -> {} {}",
374 resp.status().as_u16(),
375 resp.text()
376 );
377 if !check_eq(resp.status().as_u16(), 200, "Update user should return 200") {
378 return;
379 }
380
381 // 6. Validation: empty name
382 println!("\n6. Validation error (empty name)");
383 let resp = client
384 .post("/users")
385 .header("content-type", "application/json")
386 .body(r#"{"name": "", "email": "bad@example.com"}"#)
387 .send();
388 println!(
389 " POST /users -> {} {}",
390 resp.status().as_u16(),
391 resp.text()
392 );
393 if !check_eq(resp.status().as_u16(), 400, "Empty name should return 400") {
394 return;
395 }
396
397 // 7. Validation: invalid email
398 println!("\n7. Validation error (invalid email)");
399 let resp = client
400 .post("/users")
401 .header("content-type", "application/json")
402 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
403 .send();
404 println!(
405 " POST /users -> {} {}",
406 resp.status().as_u16(),
407 resp.text()
408 );
409 if !check_eq(
410 resp.status().as_u16(),
411 400,
412 "Invalid email should return 400",
413 ) {
414 return;
415 }
416
417 // 8. Wrong Content-Type
418 println!("\n8. Wrong Content-Type");
419 let resp = client
420 .post("/users")
421 .header("content-type", "text/plain")
422 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
423 .send();
424 println!(
425 " POST /users -> {} {}",
426 resp.status().as_u16(),
427 resp.text()
428 );
429 if !check_eq(
430 resp.status().as_u16(),
431 415,
432 "Wrong Content-Type should return 415",
433 ) {
434 return;
435 }
436
437 // 9. Delete user
438 println!("\n9. Delete user");
439 let resp = client.delete("/users/2").send();
440 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
441 if !check_eq(resp.status().as_u16(), 204, "Delete user should return 204") {
442 return;
443 }
444
445 // 10. Verify deletion
446 println!("\n10. Verify deletion");
447 let resp = client.get("/users/2").send();
448 println!(
449 " GET /users/2 -> {} {}",
450 resp.status().as_u16(),
451 resp.text()
452 );
453 if !check_eq(
454 resp.status().as_u16(),
455 404,
456 "Deleted user should return 404",
457 ) {
458 return;
459 }
460
461 let resp = client.get("/users").send();
462 println!(
463 " GET /users -> {} {}",
464 resp.status().as_u16(),
465 resp.text()
466 );
467 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
468 return;
469 }
470
471 println!("\nAll CRUD operations passed!");
472}More examples
267fn main() {
268 println!("fastapi_rust Authentication Example");
269 println!("====================================\n");
270
271 // Build the application with public and protected routes
272 let app = App::builder()
273 // Public endpoints - accessible to everyone
274 .get("/public", public_handler)
275 // Login endpoint - returns a token
276 .post("/login", login_handler)
277 // Protected endpoint - requires valid bearer token
278 .get("/protected", protected_handler)
279 .build();
280
281 println!("App created with {} route(s)\n", app.route_count());
282
283 // Create a test client
284 let client = TestClient::new(app);
285
286 // =========================================================================
287 // Test 1: Public endpoint - no auth required
288 // =========================================================================
289 println!("1. Public endpoint - no auth required");
290 let response = client.get("/public").send();
291 println!(
292 " GET /public -> {} {}",
293 response.status().as_u16(),
294 response.status().canonical_reason()
295 );
296 if !check_eq(
297 response.status().as_u16(),
298 200,
299 "GET /public should return 200",
300 ) {
301 return;
302 }
303 if !check(
304 response.text().contains("public endpoint"),
305 "GET /public should include the public endpoint body",
306 ) {
307 return;
308 }
309
310 // =========================================================================
311 // Test 2: Protected endpoint - without token (should get 401)
312 // =========================================================================
313 println!("\n2. Protected endpoint - without token");
314 let response = client.get("/protected").send();
315 println!(
316 " GET /protected -> {} {}",
317 response.status().as_u16(),
318 response.status().canonical_reason()
319 );
320 if !check_eq(
321 response.status().as_u16(),
322 401,
323 "Protected endpoint should return 401 without token",
324 ) {
325 return;
326 }
327
328 // Check for WWW-Authenticate header
329 let has_www_auth = response
330 .headers()
331 .iter()
332 .any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
333 if !check(
334 has_www_auth,
335 "401 response should include WWW-Authenticate: Bearer header",
336 ) {
337 return;
338 }
339
340 // =========================================================================
341 // Test 3: Login endpoint - get a token
342 // =========================================================================
343 println!("\n3. Login endpoint - get a token");
344 let response = client
345 .post("/login")
346 .header("content-type", "application/json")
347 .body(r#"{"username":"test","password":"test123"}"#)
348 .send();
349 println!(
350 " POST /login -> {} {}",
351 response.status().as_u16(),
352 response.status().canonical_reason()
353 );
354 if !check_eq(
355 response.status().as_u16(),
356 200,
357 "POST /login should return 200",
358 ) {
359 return;
360 }
361
362 // Parse the response to get the token
363 let body_text = response.text();
364 let body: serde_json::Value = match serde_json::from_str(body_text) {
365 Ok(body) => body,
366 Err(err) => {
367 eprintln!("Failed to parse login response JSON: {err}");
368 return;
369 }
370 };
371 let Some(bearer_value) = body.get("access_token").and_then(|value| value.as_str()) else {
372 eprintln!("Login response missing access_token");
373 return;
374 };
375 println!(" Bearer value: {bearer_value}");
376 if !check_eq(
377 bearer_value,
378 DEMO_BEARER_VALUE,
379 "Login should return the expected bearer value",
380 ) {
381 return;
382 }
383
384 // =========================================================================
385 // Test 4: Protected endpoint - with valid token (should get 200)
386 // =========================================================================
387 println!("\n4. Protected endpoint - with valid token");
388 let response = client
389 .get("/protected")
390 .header("authorization", format!("Bearer {DEMO_BEARER_VALUE}"))
391 .send();
392 println!(
393 " GET /protected (Authorization: Bearer {}) -> {} {}",
394 DEMO_BEARER_VALUE,
395 response.status().as_u16(),
396 response.status().canonical_reason()
397 );
398 if !check_eq(
399 response.status().as_u16(),
400 200,
401 "Protected endpoint should return 200 with valid token",
402 ) {
403 return;
404 }
405 if !check(
406 response.text().contains("protected resource"),
407 "Protected endpoint should include protected resource body",
408 ) {
409 return;
410 }
411
412 // =========================================================================
413 // Test 5: Protected endpoint - with invalid token (should get 403)
414 // =========================================================================
415 println!("\n5. Protected endpoint - with invalid token");
416 let response = client
417 .get("/protected")
418 .header("authorization", "Bearer wrong_token")
419 .send();
420 println!(
421 " GET /protected (Authorization: Bearer wrong_token) -> {} {}",
422 response.status().as_u16(),
423 response.status().canonical_reason()
424 );
425 if !check_eq(
426 response.status().as_u16(),
427 403,
428 "Protected endpoint should return 403 with invalid token",
429 ) {
430 return;
431 }
432
433 // =========================================================================
434 // Test 6: Protected endpoint - with wrong auth scheme (should get 401)
435 // =========================================================================
436 println!("\n6. Protected endpoint - with wrong auth scheme");
437 let response = client
438 .get("/protected")
439 .header("authorization", "Basic dXNlcjpwYXNz")
440 .send();
441 println!(
442 " GET /protected (Authorization: Basic ...) -> {} {}",
443 response.status().as_u16(),
444 response.status().canonical_reason()
445 );
446 if !check_eq(
447 response.status().as_u16(),
448 401,
449 "Protected endpoint should return 401 with wrong auth scheme",
450 ) {
451 return;
452 }
453
454 // =========================================================================
455 // Test 7: Login with wrong Content-Type (should get 415)
456 // =========================================================================
457 println!("\n7. Login with wrong Content-Type");
458 let response = client
459 .post("/login")
460 .header("content-type", "text/plain")
461 .body("demo=true")
462 .send();
463 println!(
464 " POST /login (Content-Type: text/plain) -> {} {}",
465 response.status().as_u16(),
466 response.status().canonical_reason()
467 );
468 if !check_eq(
469 response.status().as_u16(),
470 415,
471 "Login should return 415 with wrong Content-Type",
472 ) {
473 return;
474 }
475
476 // =========================================================================
477 // Test 8: Token case sensitivity (lowercase 'bearer')
478 // =========================================================================
479 println!("\n8. Token case sensitivity (lowercase 'bearer')");
480 let response = client
481 .get("/protected")
482 .header("authorization", format!("bearer {DEMO_BEARER_VALUE}"))
483 .send();
484 println!(
485 " GET /protected (Authorization: bearer {}) -> {} {}",
486 DEMO_BEARER_VALUE,
487 response.status().as_u16(),
488 response.status().canonical_reason()
489 );
490 if !check_eq(
491 response.status().as_u16(),
492 200,
493 "Bearer scheme should be case-insensitive (lowercase accepted)",
494 ) {
495 return;
496 }
497
498 println!("\nAll authentication tests passed!");
499}Sourcepub fn put<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
pub fn put<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
Adds a PUT route.
Examples found in repository?
271fn main() {
272 // Initialize the global store
273 let mut guard = STORE
274 .lock()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 *guard = Some(UserDb {
277 users: HashMap::new(),
278 next_id: 1,
279 });
280
281 println!("fastapi_rust CRUD API Example");
282 println!("=============================\n");
283
284 let app = App::builder()
285 .post("/users", create_user)
286 .get("/users", list_users)
287 .get("/users/{id}", get_user)
288 .put("/users/{id}", update_user)
289 .delete("/users/{id}", delete_user)
290 .build();
291
292 println!("App created with {} route(s)\n", app.route_count());
293 let client = TestClient::new(app);
294
295 // 1. Create users
296 println!("1. Create users");
297 let resp = client
298 .post("/users")
299 .header("content-type", "application/json")
300 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
301 .send();
302 println!(
303 " POST /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
308 return;
309 }
310
311 let resp = client
312 .post("/users")
313 .header("content-type", "application/json")
314 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
315 .send();
316 println!(
317 " POST /users -> {} {}",
318 resp.status().as_u16(),
319 resp.text()
320 );
321 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
322 return;
323 }
324
325 // 2. List users
326 println!("\n2. List all users");
327 let resp = client.get("/users").send();
328 println!(
329 " GET /users -> {} {}",
330 resp.status().as_u16(),
331 resp.text()
332 );
333 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
334 return;
335 }
336
337 // 3. Get user by ID
338 println!("\n3. Get user by ID");
339 let resp = client.get("/users/1").send();
340 println!(
341 " GET /users/1 -> {} {}",
342 resp.status().as_u16(),
343 resp.text()
344 );
345 if !check_eq(resp.status().as_u16(), 200, "Get user should return 200") {
346 return;
347 }
348
349 // 4. Get nonexistent user
350 println!("\n4. Get nonexistent user");
351 let resp = client.get("/users/999").send();
352 println!(
353 " GET /users/999 -> {} {}",
354 resp.status().as_u16(),
355 resp.text()
356 );
357 if !check_eq(
358 resp.status().as_u16(),
359 404,
360 "Missing user should return 404",
361 ) {
362 return;
363 }
364
365 // 5. Update user
366 println!("\n5. Update user");
367 let resp = client
368 .put("/users/1")
369 .header("content-type", "application/json")
370 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
371 .send();
372 println!(
373 " PUT /users/1 -> {} {}",
374 resp.status().as_u16(),
375 resp.text()
376 );
377 if !check_eq(resp.status().as_u16(), 200, "Update user should return 200") {
378 return;
379 }
380
381 // 6. Validation: empty name
382 println!("\n6. Validation error (empty name)");
383 let resp = client
384 .post("/users")
385 .header("content-type", "application/json")
386 .body(r#"{"name": "", "email": "bad@example.com"}"#)
387 .send();
388 println!(
389 " POST /users -> {} {}",
390 resp.status().as_u16(),
391 resp.text()
392 );
393 if !check_eq(resp.status().as_u16(), 400, "Empty name should return 400") {
394 return;
395 }
396
397 // 7. Validation: invalid email
398 println!("\n7. Validation error (invalid email)");
399 let resp = client
400 .post("/users")
401 .header("content-type", "application/json")
402 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
403 .send();
404 println!(
405 " POST /users -> {} {}",
406 resp.status().as_u16(),
407 resp.text()
408 );
409 if !check_eq(
410 resp.status().as_u16(),
411 400,
412 "Invalid email should return 400",
413 ) {
414 return;
415 }
416
417 // 8. Wrong Content-Type
418 println!("\n8. Wrong Content-Type");
419 let resp = client
420 .post("/users")
421 .header("content-type", "text/plain")
422 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
423 .send();
424 println!(
425 " POST /users -> {} {}",
426 resp.status().as_u16(),
427 resp.text()
428 );
429 if !check_eq(
430 resp.status().as_u16(),
431 415,
432 "Wrong Content-Type should return 415",
433 ) {
434 return;
435 }
436
437 // 9. Delete user
438 println!("\n9. Delete user");
439 let resp = client.delete("/users/2").send();
440 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
441 if !check_eq(resp.status().as_u16(), 204, "Delete user should return 204") {
442 return;
443 }
444
445 // 10. Verify deletion
446 println!("\n10. Verify deletion");
447 let resp = client.get("/users/2").send();
448 println!(
449 " GET /users/2 -> {} {}",
450 resp.status().as_u16(),
451 resp.text()
452 );
453 if !check_eq(
454 resp.status().as_u16(),
455 404,
456 "Deleted user should return 404",
457 ) {
458 return;
459 }
460
461 let resp = client.get("/users").send();
462 println!(
463 " GET /users -> {} {}",
464 resp.status().as_u16(),
465 resp.text()
466 );
467 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
468 return;
469 }
470
471 println!("\nAll CRUD operations passed!");
472}Sourcepub fn delete<H, Fut>(
self,
path: impl Into<String>,
handler: H,
) -> AppBuilder<S>
pub fn delete<H, Fut>( self, path: impl Into<String>, handler: H, ) -> AppBuilder<S>
Adds a DELETE route.
Examples found in repository?
271fn main() {
272 // Initialize the global store
273 let mut guard = STORE
274 .lock()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 *guard = Some(UserDb {
277 users: HashMap::new(),
278 next_id: 1,
279 });
280
281 println!("fastapi_rust CRUD API Example");
282 println!("=============================\n");
283
284 let app = App::builder()
285 .post("/users", create_user)
286 .get("/users", list_users)
287 .get("/users/{id}", get_user)
288 .put("/users/{id}", update_user)
289 .delete("/users/{id}", delete_user)
290 .build();
291
292 println!("App created with {} route(s)\n", app.route_count());
293 let client = TestClient::new(app);
294
295 // 1. Create users
296 println!("1. Create users");
297 let resp = client
298 .post("/users")
299 .header("content-type", "application/json")
300 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
301 .send();
302 println!(
303 " POST /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
308 return;
309 }
310
311 let resp = client
312 .post("/users")
313 .header("content-type", "application/json")
314 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
315 .send();
316 println!(
317 " POST /users -> {} {}",
318 resp.status().as_u16(),
319 resp.text()
320 );
321 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
322 return;
323 }
324
325 // 2. List users
326 println!("\n2. List all users");
327 let resp = client.get("/users").send();
328 println!(
329 " GET /users -> {} {}",
330 resp.status().as_u16(),
331 resp.text()
332 );
333 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
334 return;
335 }
336
337 // 3. Get user by ID
338 println!("\n3. Get user by ID");
339 let resp = client.get("/users/1").send();
340 println!(
341 " GET /users/1 -> {} {}",
342 resp.status().as_u16(),
343 resp.text()
344 );
345 if !check_eq(resp.status().as_u16(), 200, "Get user should return 200") {
346 return;
347 }
348
349 // 4. Get nonexistent user
350 println!("\n4. Get nonexistent user");
351 let resp = client.get("/users/999").send();
352 println!(
353 " GET /users/999 -> {} {}",
354 resp.status().as_u16(),
355 resp.text()
356 );
357 if !check_eq(
358 resp.status().as_u16(),
359 404,
360 "Missing user should return 404",
361 ) {
362 return;
363 }
364
365 // 5. Update user
366 println!("\n5. Update user");
367 let resp = client
368 .put("/users/1")
369 .header("content-type", "application/json")
370 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
371 .send();
372 println!(
373 " PUT /users/1 -> {} {}",
374 resp.status().as_u16(),
375 resp.text()
376 );
377 if !check_eq(resp.status().as_u16(), 200, "Update user should return 200") {
378 return;
379 }
380
381 // 6. Validation: empty name
382 println!("\n6. Validation error (empty name)");
383 let resp = client
384 .post("/users")
385 .header("content-type", "application/json")
386 .body(r#"{"name": "", "email": "bad@example.com"}"#)
387 .send();
388 println!(
389 " POST /users -> {} {}",
390 resp.status().as_u16(),
391 resp.text()
392 );
393 if !check_eq(resp.status().as_u16(), 400, "Empty name should return 400") {
394 return;
395 }
396
397 // 7. Validation: invalid email
398 println!("\n7. Validation error (invalid email)");
399 let resp = client
400 .post("/users")
401 .header("content-type", "application/json")
402 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
403 .send();
404 println!(
405 " POST /users -> {} {}",
406 resp.status().as_u16(),
407 resp.text()
408 );
409 if !check_eq(
410 resp.status().as_u16(),
411 400,
412 "Invalid email should return 400",
413 ) {
414 return;
415 }
416
417 // 8. Wrong Content-Type
418 println!("\n8. Wrong Content-Type");
419 let resp = client
420 .post("/users")
421 .header("content-type", "text/plain")
422 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
423 .send();
424 println!(
425 " POST /users -> {} {}",
426 resp.status().as_u16(),
427 resp.text()
428 );
429 if !check_eq(
430 resp.status().as_u16(),
431 415,
432 "Wrong Content-Type should return 415",
433 ) {
434 return;
435 }
436
437 // 9. Delete user
438 println!("\n9. Delete user");
439 let resp = client.delete("/users/2").send();
440 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
441 if !check_eq(resp.status().as_u16(), 204, "Delete user should return 204") {
442 return;
443 }
444
445 // 10. Verify deletion
446 println!("\n10. Verify deletion");
447 let resp = client.get("/users/2").send();
448 println!(
449 " GET /users/2 -> {} {}",
450 resp.status().as_u16(),
451 resp.text()
452 );
453 if !check_eq(
454 resp.status().as_u16(),
455 404,
456 "Deleted user should return 404",
457 ) {
458 return;
459 }
460
461 let resp = client.get("/users").send();
462 println!(
463 " GET /users -> {} {}",
464 resp.status().as_u16(),
465 resp.text()
466 );
467 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
468 return;
469 }
470
471 println!("\nAll CRUD operations passed!");
472}Sourcepub fn patch<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
pub fn patch<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
Adds a PATCH route.
Sourcepub fn middleware<M>(self, middleware: M) -> AppBuilder<S>where
M: Middleware + 'static,
pub fn middleware<M>(self, middleware: M) -> AppBuilder<S>where
M: Middleware + 'static,
Adds middleware to the application.
Middleware is executed in the order it is added:
beforehooks run first-to-lastafterhooks run last-to-first
Examples found in repository?
34fn main() {
35 println!("Getting Started Guide - Code Validation\n");
36
37 // === Basic App Example ===
38 println!("1. Basic app with two routes:");
39 let app = App::builder()
40 .get("/", hello)
41 .get("/health", health)
42 .build();
43
44 println!(" Routes: {}", app.route_count());
45 let client = TestClient::new(app);
46
47 let response = client.get("/").send();
48 println!(
49 " GET / -> {} ({})",
50 response.status().as_u16(),
51 response.text()
52 );
53 if !check_eq(response.status().as_u16(), 200, "GET / should return 200") {
54 return;
55 }
56 if !check_eq(response.text(), "Hello, World!", "GET / should return body") {
57 return;
58 }
59
60 let response = client.get("/health").send();
61 println!(
62 " GET /health -> {} ({})",
63 response.status().as_u16(),
64 response.text()
65 );
66 if !check_eq(
67 response.status().as_u16(),
68 200,
69 "GET /health should return 200",
70 ) {
71 return;
72 }
73
74 // === App with Middleware ===
75 println!("\n2. App with middleware:");
76 let app = App::builder()
77 .middleware(RequestIdMiddleware::new())
78 .middleware(SecurityHeaders::new())
79 .get("/", hello)
80 .build();
81
82 let client = TestClient::new(app);
83 let response = client.get("/").send();
84 println!(" GET / -> {}", response.status().as_u16());
85 if !check_eq(
86 response.status().as_u16(),
87 200,
88 "GET / with middleware should return 200",
89 ) {
90 return;
91 }
92
93 // === App with Configuration ===
94 println!("\n3. App with configuration:");
95 let config = AppConfig::new()
96 .name("My API")
97 .version("1.0.0")
98 .debug(true)
99 .max_body_size(10 * 1024 * 1024)
100 .request_timeout_ms(30_000);
101
102 let app = App::builder().config(config).get("/", hello).build();
103
104 println!(" App name: {}", app.config().name);
105 println!(" Version: {}", app.config().version);
106 if !check_eq(
107 app.config().name.as_str(),
108 "My API",
109 "Config name should match",
110 ) {
111 return;
112 }
113 if !check_eq(
114 app.config().version.as_str(),
115 "1.0.0",
116 "Config version should match",
117 ) {
118 return;
119 }
120
121 // === 404 for unknown routes ===
122 println!("\n4. 404 for unknown routes:");
123 let app = App::builder().get("/", hello).build();
124
125 let client = TestClient::new(app);
126 let response = client.get("/nonexistent").send();
127 println!(" GET /nonexistent -> {}", response.status().as_u16());
128 if !check_eq(
129 response.status().as_u16(),
130 404,
131 "Unknown routes should return 404",
132 ) {
133 return;
134 }
135
136 println!("\nAll getting started examples validated successfully!");
137}Sourcepub fn include_router(self, router: APIRouter) -> AppBuilder<S>
pub fn include_router(self, router: APIRouter) -> AppBuilder<S>
Includes routes from an APIRouter.
This adds all routes from the router to the application, applying the router’s prefix, tags, and dependencies.
§Example
use fastapi_core::api_router::APIRouter;
let users_router = APIRouter::new()
.prefix("/users")
.get("", list_users)
.get("/{id}", get_user);
let app = App::builder()
.include_router(users_router)
.build();Sourcepub fn include_router_with_config(
self,
router: APIRouter,
config: IncludeConfig,
) -> AppBuilder<S>
pub fn include_router_with_config( self, router: APIRouter, config: IncludeConfig, ) -> AppBuilder<S>
Includes routes from an APIRouter with configuration.
This allows applying additional configuration when including a router, such as prepending a prefix, adding tags, or injecting dependencies.
§Example
use fastapi_core::api_router::{APIRouter, IncludeConfig};
let users_router = APIRouter::new()
.prefix("/users")
.get("", list_users);
let config = IncludeConfig::new()
.prefix("/api/v1")
.tags(vec!["api"]);
let app = App::builder()
.include_router_with_config(users_router, config)
.build();Sourcepub fn mount(self, prefix: impl Into<String>, app: App) -> AppBuilder<S>
pub fn mount(self, prefix: impl Into<String>, app: App) -> AppBuilder<S>
Mounts a sub-application at a path prefix.
Mounted applications are completely independent from the parent:
- They have their own middleware stack (parent middleware does NOT apply)
- They have their own state and configuration
- Their OpenAPI schemas are NOT merged with the parent
- Request paths have the prefix stripped before being passed to the sub-app
This differs from include_router, which merges
routes into the parent app and applies parent middleware.
§Use Cases
- Mount admin panels at
/admin - Mount Swagger UI at
/docs - Mount static file servers
- Integrate legacy or third-party apps
§Path Stripping Behavior
When a request arrives at /admin/users, and an app is mounted at /admin,
the sub-app receives the request with path /users.
§Example
use fastapi_core::app::App;
// Create an admin sub-application
let admin_app = App::builder()
.get("/users", admin_list_users)
.get("/settings", admin_settings)
.middleware(AdminAuthMiddleware::new())
.build();
// Mount it at /admin
let main_app = App::builder()
.get("/", home_page)
.mount("/admin", admin_app)
.build();
// Now:
// - GET / -> home_page
// - GET /admin/users -> admin_list_users (with AdminAuthMiddleware)
// - GET /admin/settings -> admin_settings (with AdminAuthMiddleware)Sourcepub fn mounted_app_count(&self) -> usize
pub fn mounted_app_count(&self) -> usize
Returns the number of mounted sub-applications.
Sourcepub fn state<T>(self, value: T) -> AppBuilder<S>
👎Deprecated since 0.2.0: Use with_state for compile-time state type verification
pub fn state<T>(self, value: T) -> AppBuilder<S>
with_state for compile-time state type verificationAdds shared state to the application (legacy method).
State can be accessed by handlers through the State<T> extractor.
Note: This method is deprecated in favor of with_state,
which provides compile-time verification that state types are registered.
Sourcepub fn with_state<T>(self, value: T) -> AppBuilder<(T, S)>
pub fn with_state<T>(self, value: T) -> AppBuilder<(T, S)>
Adds typed state to the application with compile-time registration.
This method registers state using a type-state pattern, which enables compile-time verification that state types are properly registered before they are used by handlers.
The return type changes from AppBuilder<S> to AppBuilder<(T, S)>,
recording that type T is now available in the state registry.
§Example
use fastapi_core::app::App;
struct DbPool { /* ... */ }
struct Config { api_key: String }
// Type evolves: () -> (DbPool, ()) -> (Config, (DbPool, ()))
let app = App::builder()
.with_state(DbPool::new()) // Now has DbPool
.with_state(Config::default()) // Now has DbPool + Config
.build();§Compile-Time Safety
When used with the RequiresState trait, handlers can declare their
state dependencies and the compiler will verify they are met:
// This handler requires DbPool to be registered
fn handler_requiring_db<S: HasState<DbPool>>(app: AppBuilder<S>) { /* ... */ }Sourcepub fn override_dependency<T, F, Fut>(self, f: F) -> AppBuilder<S>where
T: FromDependency,
F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<T, <T as FromDependency>::Error>> + Send + 'static,
pub fn override_dependency<T, F, Fut>(self, f: F) -> AppBuilder<S>where
T: FromDependency,
F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<T, <T as FromDependency>::Error>> + Send + 'static,
Registers a dependency override for this application (useful in tests).
Sourcepub fn override_dependency_value<T>(self, value: T) -> AppBuilder<S>where
T: FromDependency,
pub fn override_dependency_value<T>(self, value: T) -> AppBuilder<S>where
T: FromDependency,
Registers a fixed dependency override value.
Sourcepub fn clear_dependency_overrides(self) -> AppBuilder<S>
pub fn clear_dependency_overrides(self) -> AppBuilder<S>
Clears all registered dependency overrides.
Sourcepub fn exception_handler<E, H>(self, handler: H) -> AppBuilder<S>
pub fn exception_handler<E, H>(self, handler: H) -> AppBuilder<S>
Registers a custom exception handler for a specific error type.
When an error of type E occurs during request handling, the registered
handler will be called to convert it into a response.
§Example
#[derive(Debug)]
struct AuthError(String);
impl std::fmt::Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Auth error: {}", self.0)
}
}
impl std::error::Error for AuthError {}
let app = App::builder()
.exception_handler(|_ctx, err: AuthError| {
Response::with_status(StatusCode::UNAUTHORIZED)
.header("www-authenticate", b"Bearer".to_vec())
.body_json(&json!({"error": err.0}))
})
.build();Sourcepub fn exception_handlers(self, handlers: ExceptionHandlers) -> AppBuilder<S>
pub fn exception_handlers(self, handlers: ExceptionHandlers) -> AppBuilder<S>
Sets the exception handlers registry.
This replaces any previously registered handlers.
Sourcepub fn with_default_exception_handlers(self) -> AppBuilder<S>
pub fn with_default_exception_handlers(self) -> AppBuilder<S>
Uses default exception handlers for common error types.
This registers handlers for:
HttpError→ JSON response with status/detailValidationErrors→ 422 with error list
Sourcepub fn on_startup<F>(self, hook: F) -> AppBuilder<S>
pub fn on_startup<F>(self, hook: F) -> AppBuilder<S>
Registers a synchronous startup hook.
Startup hooks run before the server starts accepting connections, in the order they are registered (FIFO).
§Example
let app = App::builder()
.on_startup(|| {
println!("Connecting to database...");
Ok(())
})
.on_startup(|| {
println!("Loading configuration...");
Ok(())
})
.build();Sourcepub fn on_startup_async<F, Fut>(self, hook: F) -> AppBuilder<S>
pub fn on_startup_async<F, Fut>(self, hook: F) -> AppBuilder<S>
Sourcepub fn on_shutdown<F>(self, hook: F) -> AppBuilder<S>
pub fn on_shutdown<F>(self, hook: F) -> AppBuilder<S>
Registers a synchronous shutdown hook.
Shutdown hooks run after the server stops accepting connections and all in-flight requests complete (or are cancelled).
Shutdown hooks run in reverse registration order (LIFO), matching typical resource cleanup patterns (last acquired, first released).
§Example
let app = App::builder()
.on_shutdown(|| {
println!("Closing database connections...");
})
.build();Sourcepub fn on_shutdown_async<F, Fut>(self, hook: F) -> AppBuilder<S>
pub fn on_shutdown_async<F, Fut>(self, hook: F) -> AppBuilder<S>
Sourcepub fn lifespan<F, Fut, T>(self, lifespan_fn: F) -> AppBuilder<S>where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<LifespanScope<T>, LifespanError>> + Send + 'static,
T: Send + Sync + 'static,
pub fn lifespan<F, Fut, T>(self, lifespan_fn: F) -> AppBuilder<S>where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<LifespanScope<T>, LifespanError>> + Send + 'static,
T: Send + Sync + 'static,
Registers a lifespan context manager for async startup/shutdown.
The lifespan pattern is preferred over separate on_startup/on_shutdown hooks
because it allows sharing state between the startup and shutdown phases. This is
especially useful for resources like database connections, HTTP clients, or
background task managers.
The lifespan function runs during application startup. It should:
- Initialize resources (connect to database, start background tasks, etc.)
- Return a
LifespanScopecontaining:- State to be added to the application (accessible via
State<T>extractor) - An optional cleanup closure to run during shutdown
- State to be added to the application (accessible via
If the lifespan function returns an error, application startup is aborted.
Note: When a lifespan is provided, it runs before any on_startup hooks.
The lifespan cleanup runs after all on_shutdown hooks during shutdown.
§Example
use fastapi_core::app::{App, LifespanScope, LifespanError};
#[derive(Clone)]
struct DatabasePool { /* ... */ }
impl DatabasePool {
async fn connect(url: &str) -> Result<Self, Error> { /* ... */ }
async fn close(&self) { /* ... */ }
}
let app = App::builder()
.lifespan(|| async {
// Startup: connect to database
println!("Connecting to database...");
let pool = DatabasePool::connect("postgres://localhost/mydb")
.await
.map_err(|e| LifespanError::with_source("database connection failed", e))?;
// Clone for use in cleanup
let pool_for_cleanup = pool.clone();
// Return state and cleanup
Ok(LifespanScope::new(pool)
.on_shutdown(async move {
println!("Closing database connections...");
pool_for_cleanup.close().await;
}))
})
.get("/users", get_users) // Handler can use State<DatabasePool>
.build();§Multiple State Types
To provide multiple state types from a single lifespan, use a tuple or define a struct containing all your state:
#[derive(Clone)]
struct AppState {
db: DatabasePool,
cache: RedisClient,
config: AppConfig,
}
let app = App::builder()
.lifespan(|| async {
let db = DatabasePool::connect("...").await?;
let cache = RedisClient::connect("...").await?;
let config = load_config().await?;
let state = AppState { db, cache, config };
let state_for_cleanup = state.clone();
Ok(LifespanScope::new(state)
.on_shutdown(async move {
state_for_cleanup.db.close().await;
state_for_cleanup.cache.close().await;
}))
})
.build();Sourcepub fn has_lifespan(&self) -> bool
pub fn has_lifespan(&self) -> bool
Returns true if a lifespan function has been registered.
Sourcepub fn startup_hook_count(&self) -> usize
pub fn startup_hook_count(&self) -> usize
Returns the number of registered startup hooks.
Sourcepub fn shutdown_hook_count(&self) -> usize
pub fn shutdown_hook_count(&self) -> usize
Returns the number of registered shutdown hooks.
Sourcepub fn build(self) -> App
pub fn build(self) -> App
Builds the application.
This consumes the builder and returns the configured App.
Examples found in repository?
61fn main() {
62 println!("fastapi_rust Hello World Example");
63 println!("================================\n");
64
65 // Build the application
66 //
67 // App::builder() creates a new application builder that lets you:
68 // - Add routes for different HTTP methods
69 // - Configure middleware
70 // - Set application state
71 // - Define exception handlers
72 let app = App::builder()
73 // Register a GET handler for the root path "/"
74 //
75 // This is equivalent to:
76 // @app.get("/")
77 // def hello():
78 // return "Hello, World!"
79 // in Python FastAPI
80 .get("/", hello_handler)
81 // Build the final immutable App
82 .build();
83
84 println!("App created with {} route(s)\n", app.route_count());
85
86 // Create a test client to make requests to our app
87 //
88 // TestClient wraps any Handler (including App) and provides
89 // a convenient API for making HTTP requests in tests.
90 let client = TestClient::new(app);
91
92 // Make a GET request to "/"
93 println!("Making request: GET /");
94 let response = client.get("/").send();
95
96 // Check the response
97 println!(
98 "GET / -> {} {}",
99 response.status().as_u16(),
100 response.status().canonical_reason()
101 );
102 println!("Response: {}\n", response.text());
103
104 // Verify success
105 if !check_eq(response.status().as_u16(), 200, "GET / should return 200") {
106 return;
107 }
108 if !check_eq(response.text(), "Hello, World!", "GET / should return body") {
109 return;
110 }
111
112 // Try a path that doesn't exist - should get 404
113 println!("Making request: GET /not-found");
114 let response = client.get("/not-found").send();
115 println!(
116 "GET /not-found -> {} {}",
117 response.status().as_u16(),
118 response.status().canonical_reason()
119 );
120 if !check_eq(
121 response.status().as_u16(),
122 404,
123 "Unknown routes should return 404",
124 ) {
125 return;
126 }
127
128 println!("\nAll assertions passed!");
129}More examples
34fn main() {
35 println!("Getting Started Guide - Code Validation\n");
36
37 // === Basic App Example ===
38 println!("1. Basic app with two routes:");
39 let app = App::builder()
40 .get("/", hello)
41 .get("/health", health)
42 .build();
43
44 println!(" Routes: {}", app.route_count());
45 let client = TestClient::new(app);
46
47 let response = client.get("/").send();
48 println!(
49 " GET / -> {} ({})",
50 response.status().as_u16(),
51 response.text()
52 );
53 if !check_eq(response.status().as_u16(), 200, "GET / should return 200") {
54 return;
55 }
56 if !check_eq(response.text(), "Hello, World!", "GET / should return body") {
57 return;
58 }
59
60 let response = client.get("/health").send();
61 println!(
62 " GET /health -> {} ({})",
63 response.status().as_u16(),
64 response.text()
65 );
66 if !check_eq(
67 response.status().as_u16(),
68 200,
69 "GET /health should return 200",
70 ) {
71 return;
72 }
73
74 // === App with Middleware ===
75 println!("\n2. App with middleware:");
76 let app = App::builder()
77 .middleware(RequestIdMiddleware::new())
78 .middleware(SecurityHeaders::new())
79 .get("/", hello)
80 .build();
81
82 let client = TestClient::new(app);
83 let response = client.get("/").send();
84 println!(" GET / -> {}", response.status().as_u16());
85 if !check_eq(
86 response.status().as_u16(),
87 200,
88 "GET / with middleware should return 200",
89 ) {
90 return;
91 }
92
93 // === App with Configuration ===
94 println!("\n3. App with configuration:");
95 let config = AppConfig::new()
96 .name("My API")
97 .version("1.0.0")
98 .debug(true)
99 .max_body_size(10 * 1024 * 1024)
100 .request_timeout_ms(30_000);
101
102 let app = App::builder().config(config).get("/", hello).build();
103
104 println!(" App name: {}", app.config().name);
105 println!(" Version: {}", app.config().version);
106 if !check_eq(
107 app.config().name.as_str(),
108 "My API",
109 "Config name should match",
110 ) {
111 return;
112 }
113 if !check_eq(
114 app.config().version.as_str(),
115 "1.0.0",
116 "Config version should match",
117 ) {
118 return;
119 }
120
121 // === 404 for unknown routes ===
122 println!("\n4. 404 for unknown routes:");
123 let app = App::builder().get("/", hello).build();
124
125 let client = TestClient::new(app);
126 let response = client.get("/nonexistent").send();
127 println!(" GET /nonexistent -> {}", response.status().as_u16());
128 if !check_eq(
129 response.status().as_u16(),
130 404,
131 "Unknown routes should return 404",
132 ) {
133 return;
134 }
135
136 println!("\nAll getting started examples validated successfully!");
137}271fn main() {
272 // Initialize the global store
273 let mut guard = STORE
274 .lock()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 *guard = Some(UserDb {
277 users: HashMap::new(),
278 next_id: 1,
279 });
280
281 println!("fastapi_rust CRUD API Example");
282 println!("=============================\n");
283
284 let app = App::builder()
285 .post("/users", create_user)
286 .get("/users", list_users)
287 .get("/users/{id}", get_user)
288 .put("/users/{id}", update_user)
289 .delete("/users/{id}", delete_user)
290 .build();
291
292 println!("App created with {} route(s)\n", app.route_count());
293 let client = TestClient::new(app);
294
295 // 1. Create users
296 println!("1. Create users");
297 let resp = client
298 .post("/users")
299 .header("content-type", "application/json")
300 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
301 .send();
302 println!(
303 " POST /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
308 return;
309 }
310
311 let resp = client
312 .post("/users")
313 .header("content-type", "application/json")
314 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
315 .send();
316 println!(
317 " POST /users -> {} {}",
318 resp.status().as_u16(),
319 resp.text()
320 );
321 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
322 return;
323 }
324
325 // 2. List users
326 println!("\n2. List all users");
327 let resp = client.get("/users").send();
328 println!(
329 " GET /users -> {} {}",
330 resp.status().as_u16(),
331 resp.text()
332 );
333 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
334 return;
335 }
336
337 // 3. Get user by ID
338 println!("\n3. Get user by ID");
339 let resp = client.get("/users/1").send();
340 println!(
341 " GET /users/1 -> {} {}",
342 resp.status().as_u16(),
343 resp.text()
344 );
345 if !check_eq(resp.status().as_u16(), 200, "Get user should return 200") {
346 return;
347 }
348
349 // 4. Get nonexistent user
350 println!("\n4. Get nonexistent user");
351 let resp = client.get("/users/999").send();
352 println!(
353 " GET /users/999 -> {} {}",
354 resp.status().as_u16(),
355 resp.text()
356 );
357 if !check_eq(
358 resp.status().as_u16(),
359 404,
360 "Missing user should return 404",
361 ) {
362 return;
363 }
364
365 // 5. Update user
366 println!("\n5. Update user");
367 let resp = client
368 .put("/users/1")
369 .header("content-type", "application/json")
370 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
371 .send();
372 println!(
373 " PUT /users/1 -> {} {}",
374 resp.status().as_u16(),
375 resp.text()
376 );
377 if !check_eq(resp.status().as_u16(), 200, "Update user should return 200") {
378 return;
379 }
380
381 // 6. Validation: empty name
382 println!("\n6. Validation error (empty name)");
383 let resp = client
384 .post("/users")
385 .header("content-type", "application/json")
386 .body(r#"{"name": "", "email": "bad@example.com"}"#)
387 .send();
388 println!(
389 " POST /users -> {} {}",
390 resp.status().as_u16(),
391 resp.text()
392 );
393 if !check_eq(resp.status().as_u16(), 400, "Empty name should return 400") {
394 return;
395 }
396
397 // 7. Validation: invalid email
398 println!("\n7. Validation error (invalid email)");
399 let resp = client
400 .post("/users")
401 .header("content-type", "application/json")
402 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
403 .send();
404 println!(
405 " POST /users -> {} {}",
406 resp.status().as_u16(),
407 resp.text()
408 );
409 if !check_eq(
410 resp.status().as_u16(),
411 400,
412 "Invalid email should return 400",
413 ) {
414 return;
415 }
416
417 // 8. Wrong Content-Type
418 println!("\n8. Wrong Content-Type");
419 let resp = client
420 .post("/users")
421 .header("content-type", "text/plain")
422 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
423 .send();
424 println!(
425 " POST /users -> {} {}",
426 resp.status().as_u16(),
427 resp.text()
428 );
429 if !check_eq(
430 resp.status().as_u16(),
431 415,
432 "Wrong Content-Type should return 415",
433 ) {
434 return;
435 }
436
437 // 9. Delete user
438 println!("\n9. Delete user");
439 let resp = client.delete("/users/2").send();
440 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
441 if !check_eq(resp.status().as_u16(), 204, "Delete user should return 204") {
442 return;
443 }
444
445 // 10. Verify deletion
446 println!("\n10. Verify deletion");
447 let resp = client.get("/users/2").send();
448 println!(
449 " GET /users/2 -> {} {}",
450 resp.status().as_u16(),
451 resp.text()
452 );
453 if !check_eq(
454 resp.status().as_u16(),
455 404,
456 "Deleted user should return 404",
457 ) {
458 return;
459 }
460
461 let resp = client.get("/users").send();
462 println!(
463 " GET /users -> {} {}",
464 resp.status().as_u16(),
465 resp.text()
466 );
467 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
468 return;
469 }
470
471 println!("\nAll CRUD operations passed!");
472}267fn main() {
268 println!("fastapi_rust Authentication Example");
269 println!("====================================\n");
270
271 // Build the application with public and protected routes
272 let app = App::builder()
273 // Public endpoints - accessible to everyone
274 .get("/public", public_handler)
275 // Login endpoint - returns a token
276 .post("/login", login_handler)
277 // Protected endpoint - requires valid bearer token
278 .get("/protected", protected_handler)
279 .build();
280
281 println!("App created with {} route(s)\n", app.route_count());
282
283 // Create a test client
284 let client = TestClient::new(app);
285
286 // =========================================================================
287 // Test 1: Public endpoint - no auth required
288 // =========================================================================
289 println!("1. Public endpoint - no auth required");
290 let response = client.get("/public").send();
291 println!(
292 " GET /public -> {} {}",
293 response.status().as_u16(),
294 response.status().canonical_reason()
295 );
296 if !check_eq(
297 response.status().as_u16(),
298 200,
299 "GET /public should return 200",
300 ) {
301 return;
302 }
303 if !check(
304 response.text().contains("public endpoint"),
305 "GET /public should include the public endpoint body",
306 ) {
307 return;
308 }
309
310 // =========================================================================
311 // Test 2: Protected endpoint - without token (should get 401)
312 // =========================================================================
313 println!("\n2. Protected endpoint - without token");
314 let response = client.get("/protected").send();
315 println!(
316 " GET /protected -> {} {}",
317 response.status().as_u16(),
318 response.status().canonical_reason()
319 );
320 if !check_eq(
321 response.status().as_u16(),
322 401,
323 "Protected endpoint should return 401 without token",
324 ) {
325 return;
326 }
327
328 // Check for WWW-Authenticate header
329 let has_www_auth = response
330 .headers()
331 .iter()
332 .any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
333 if !check(
334 has_www_auth,
335 "401 response should include WWW-Authenticate: Bearer header",
336 ) {
337 return;
338 }
339
340 // =========================================================================
341 // Test 3: Login endpoint - get a token
342 // =========================================================================
343 println!("\n3. Login endpoint - get a token");
344 let response = client
345 .post("/login")
346 .header("content-type", "application/json")
347 .body(r#"{"username":"test","password":"test123"}"#)
348 .send();
349 println!(
350 " POST /login -> {} {}",
351 response.status().as_u16(),
352 response.status().canonical_reason()
353 );
354 if !check_eq(
355 response.status().as_u16(),
356 200,
357 "POST /login should return 200",
358 ) {
359 return;
360 }
361
362 // Parse the response to get the token
363 let body_text = response.text();
364 let body: serde_json::Value = match serde_json::from_str(body_text) {
365 Ok(body) => body,
366 Err(err) => {
367 eprintln!("Failed to parse login response JSON: {err}");
368 return;
369 }
370 };
371 let Some(bearer_value) = body.get("access_token").and_then(|value| value.as_str()) else {
372 eprintln!("Login response missing access_token");
373 return;
374 };
375 println!(" Bearer value: {bearer_value}");
376 if !check_eq(
377 bearer_value,
378 DEMO_BEARER_VALUE,
379 "Login should return the expected bearer value",
380 ) {
381 return;
382 }
383
384 // =========================================================================
385 // Test 4: Protected endpoint - with valid token (should get 200)
386 // =========================================================================
387 println!("\n4. Protected endpoint - with valid token");
388 let response = client
389 .get("/protected")
390 .header("authorization", format!("Bearer {DEMO_BEARER_VALUE}"))
391 .send();
392 println!(
393 " GET /protected (Authorization: Bearer {}) -> {} {}",
394 DEMO_BEARER_VALUE,
395 response.status().as_u16(),
396 response.status().canonical_reason()
397 );
398 if !check_eq(
399 response.status().as_u16(),
400 200,
401 "Protected endpoint should return 200 with valid token",
402 ) {
403 return;
404 }
405 if !check(
406 response.text().contains("protected resource"),
407 "Protected endpoint should include protected resource body",
408 ) {
409 return;
410 }
411
412 // =========================================================================
413 // Test 5: Protected endpoint - with invalid token (should get 403)
414 // =========================================================================
415 println!("\n5. Protected endpoint - with invalid token");
416 let response = client
417 .get("/protected")
418 .header("authorization", "Bearer wrong_token")
419 .send();
420 println!(
421 " GET /protected (Authorization: Bearer wrong_token) -> {} {}",
422 response.status().as_u16(),
423 response.status().canonical_reason()
424 );
425 if !check_eq(
426 response.status().as_u16(),
427 403,
428 "Protected endpoint should return 403 with invalid token",
429 ) {
430 return;
431 }
432
433 // =========================================================================
434 // Test 6: Protected endpoint - with wrong auth scheme (should get 401)
435 // =========================================================================
436 println!("\n6. Protected endpoint - with wrong auth scheme");
437 let response = client
438 .get("/protected")
439 .header("authorization", "Basic dXNlcjpwYXNz")
440 .send();
441 println!(
442 " GET /protected (Authorization: Basic ...) -> {} {}",
443 response.status().as_u16(),
444 response.status().canonical_reason()
445 );
446 if !check_eq(
447 response.status().as_u16(),
448 401,
449 "Protected endpoint should return 401 with wrong auth scheme",
450 ) {
451 return;
452 }
453
454 // =========================================================================
455 // Test 7: Login with wrong Content-Type (should get 415)
456 // =========================================================================
457 println!("\n7. Login with wrong Content-Type");
458 let response = client
459 .post("/login")
460 .header("content-type", "text/plain")
461 .body("demo=true")
462 .send();
463 println!(
464 " POST /login (Content-Type: text/plain) -> {} {}",
465 response.status().as_u16(),
466 response.status().canonical_reason()
467 );
468 if !check_eq(
469 response.status().as_u16(),
470 415,
471 "Login should return 415 with wrong Content-Type",
472 ) {
473 return;
474 }
475
476 // =========================================================================
477 // Test 8: Token case sensitivity (lowercase 'bearer')
478 // =========================================================================
479 println!("\n8. Token case sensitivity (lowercase 'bearer')");
480 let response = client
481 .get("/protected")
482 .header("authorization", format!("bearer {DEMO_BEARER_VALUE}"))
483 .send();
484 println!(
485 " GET /protected (Authorization: bearer {}) -> {} {}",
486 DEMO_BEARER_VALUE,
487 response.status().as_u16(),
488 response.status().canonical_reason()
489 );
490 if !check_eq(
491 response.status().as_u16(),
492 200,
493 "Bearer scheme should be case-insensitive (lowercase accepted)",
494 ) {
495 return;
496 }
497
498 println!("\nAll authentication tests passed!");
499}