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?
24fn main() {
25 println!("Getting Started Guide - Code Validation\n");
26
27 // === Basic App Example ===
28 println!("1. Basic app with two routes:");
29 let app = App::builder()
30 .get("/", hello)
31 .get("/health", health)
32 .build();
33
34 println!(" Routes: {}", app.route_count());
35 let client = TestClient::new(app);
36
37 let response = client.get("/").send();
38 println!(
39 " GET / -> {} ({})",
40 response.status().as_u16(),
41 response.text()
42 );
43 assert_eq!(response.status().as_u16(), 200);
44 assert_eq!(response.text(), "Hello, World!");
45
46 let response = client.get("/health").send();
47 println!(
48 " GET /health -> {} ({})",
49 response.status().as_u16(),
50 response.text()
51 );
52 assert_eq!(response.status().as_u16(), 200);
53
54 // === App with Middleware ===
55 println!("\n2. App with middleware:");
56 let app = App::builder()
57 .middleware(RequestIdMiddleware::new())
58 .middleware(SecurityHeaders::new())
59 .get("/", hello)
60 .build();
61
62 let client = TestClient::new(app);
63 let response = client.get("/").send();
64 println!(" GET / -> {}", response.status().as_u16());
65 assert_eq!(response.status().as_u16(), 200);
66
67 // === App with Configuration ===
68 println!("\n3. App with configuration:");
69 let config = AppConfig::new()
70 .name("My API")
71 .version("1.0.0")
72 .debug(true)
73 .max_body_size(10 * 1024 * 1024)
74 .request_timeout_ms(30_000);
75
76 let app = App::builder().config(config).get("/", hello).build();
77
78 println!(" App name: {}", app.config().name);
79 println!(" Version: {}", app.config().version);
80 assert_eq!(app.config().name, "My API");
81 assert_eq!(app.config().version, "1.0.0");
82
83 // === 404 for unknown routes ===
84 println!("\n4. 404 for unknown routes:");
85 let app = App::builder().get("/", hello).build();
86
87 let client = TestClient::new(app);
88 let response = client.get("/nonexistent").send();
89 println!(" GET /nonexistent -> {}", response.status().as_u16());
90 assert_eq!(response.status().as_u16(), 404);
91
92 println!("\nAll getting started examples validated successfully!");
93}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?
51fn main() {
52 println!("fastapi_rust Hello World Example");
53 println!("================================\n");
54
55 // Build the application
56 //
57 // App::builder() creates a new application builder that lets you:
58 // - Add routes for different HTTP methods
59 // - Configure middleware
60 // - Set application state
61 // - Define exception handlers
62 let app = App::builder()
63 // Register a GET handler for the root path "/"
64 //
65 // This is equivalent to:
66 // @app.get("/")
67 // def hello():
68 // return "Hello, World!"
69 // in Python FastAPI
70 .get("/", hello_handler)
71 // Build the final immutable App
72 .build();
73
74 println!("App created with {} route(s)\n", app.route_count());
75
76 // Create a test client to make requests to our app
77 //
78 // TestClient wraps any Handler (including App) and provides
79 // a convenient API for making HTTP requests in tests.
80 let client = TestClient::new(app);
81
82 // Make a GET request to "/"
83 println!("Making request: GET /");
84 let response = client.get("/").send();
85
86 // Check the response
87 println!(
88 "GET / -> {} {}",
89 response.status().as_u16(),
90 response.status().canonical_reason()
91 );
92 println!("Response: {}\n", response.text());
93
94 // Verify success
95 assert_eq!(response.status().as_u16(), 200);
96 assert_eq!(response.text(), "Hello, World!");
97
98 // Try a path that doesn't exist - should get 404
99 println!("Making request: GET /not-found");
100 let response = client.get("/not-found").send();
101 println!(
102 "GET /not-found -> {} {}",
103 response.status().as_u16(),
104 response.status().canonical_reason()
105 );
106 assert_eq!(response.status().as_u16(), 404);
107
108 println!("\nAll assertions passed!");
109}More examples
24fn main() {
25 println!("Getting Started Guide - Code Validation\n");
26
27 // === Basic App Example ===
28 println!("1. Basic app with two routes:");
29 let app = App::builder()
30 .get("/", hello)
31 .get("/health", health)
32 .build();
33
34 println!(" Routes: {}", app.route_count());
35 let client = TestClient::new(app);
36
37 let response = client.get("/").send();
38 println!(
39 " GET / -> {} ({})",
40 response.status().as_u16(),
41 response.text()
42 );
43 assert_eq!(response.status().as_u16(), 200);
44 assert_eq!(response.text(), "Hello, World!");
45
46 let response = client.get("/health").send();
47 println!(
48 " GET /health -> {} ({})",
49 response.status().as_u16(),
50 response.text()
51 );
52 assert_eq!(response.status().as_u16(), 200);
53
54 // === App with Middleware ===
55 println!("\n2. App with middleware:");
56 let app = App::builder()
57 .middleware(RequestIdMiddleware::new())
58 .middleware(SecurityHeaders::new())
59 .get("/", hello)
60 .build();
61
62 let client = TestClient::new(app);
63 let response = client.get("/").send();
64 println!(" GET / -> {}", response.status().as_u16());
65 assert_eq!(response.status().as_u16(), 200);
66
67 // === App with Configuration ===
68 println!("\n3. App with configuration:");
69 let config = AppConfig::new()
70 .name("My API")
71 .version("1.0.0")
72 .debug(true)
73 .max_body_size(10 * 1024 * 1024)
74 .request_timeout_ms(30_000);
75
76 let app = App::builder().config(config).get("/", hello).build();
77
78 println!(" App name: {}", app.config().name);
79 println!(" Version: {}", app.config().version);
80 assert_eq!(app.config().name, "My API");
81 assert_eq!(app.config().version, "1.0.0");
82
83 // === 404 for unknown routes ===
84 println!("\n4. 404 for unknown routes:");
85 let app = App::builder().get("/", hello).build();
86
87 let client = TestClient::new(app);
88 let response = client.get("/nonexistent").send();
89 println!(" GET /nonexistent -> {}", response.status().as_u16());
90 assert_eq!(response.status().as_u16(), 404);
91
92 println!("\nAll getting started examples validated successfully!");
93}252fn main() {
253 // Initialize the global store
254 *STORE.lock().unwrap() = Some(UserDb {
255 users: HashMap::new(),
256 next_id: 1,
257 });
258
259 println!("fastapi_rust CRUD API Example");
260 println!("=============================\n");
261
262 let app = App::builder()
263 .post("/users", create_user)
264 .get("/users", list_users)
265 .get("/users/{id}", get_user)
266 .put("/users/{id}", update_user)
267 .delete("/users/{id}", delete_user)
268 .build();
269
270 println!("App created with {} route(s)\n", app.route_count());
271 let client = TestClient::new(app);
272
273 // 1. Create users
274 println!("1. Create users");
275 let resp = client
276 .post("/users")
277 .header("content-type", "application/json")
278 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
279 .send();
280 println!(
281 " POST /users -> {} {}",
282 resp.status().as_u16(),
283 resp.text()
284 );
285 assert_eq!(resp.status().as_u16(), 201);
286
287 let resp = client
288 .post("/users")
289 .header("content-type", "application/json")
290 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
291 .send();
292 println!(
293 " POST /users -> {} {}",
294 resp.status().as_u16(),
295 resp.text()
296 );
297 assert_eq!(resp.status().as_u16(), 201);
298
299 // 2. List users
300 println!("\n2. List all users");
301 let resp = client.get("/users").send();
302 println!(
303 " GET /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 assert_eq!(resp.status().as_u16(), 200);
308
309 // 3. Get user by ID
310 println!("\n3. Get user by ID");
311 let resp = client.get("/users/1").send();
312 println!(
313 " GET /users/1 -> {} {}",
314 resp.status().as_u16(),
315 resp.text()
316 );
317 assert_eq!(resp.status().as_u16(), 200);
318
319 // 4. Get nonexistent user
320 println!("\n4. Get nonexistent user");
321 let resp = client.get("/users/999").send();
322 println!(
323 " GET /users/999 -> {} {}",
324 resp.status().as_u16(),
325 resp.text()
326 );
327 assert_eq!(resp.status().as_u16(), 404);
328
329 // 5. Update user
330 println!("\n5. Update user");
331 let resp = client
332 .put("/users/1")
333 .header("content-type", "application/json")
334 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
335 .send();
336 println!(
337 " PUT /users/1 -> {} {}",
338 resp.status().as_u16(),
339 resp.text()
340 );
341 assert_eq!(resp.status().as_u16(), 200);
342
343 // 6. Validation: empty name
344 println!("\n6. Validation error (empty name)");
345 let resp = client
346 .post("/users")
347 .header("content-type", "application/json")
348 .body(r#"{"name": "", "email": "bad@example.com"}"#)
349 .send();
350 println!(
351 " POST /users -> {} {}",
352 resp.status().as_u16(),
353 resp.text()
354 );
355 assert_eq!(resp.status().as_u16(), 400);
356
357 // 7. Validation: invalid email
358 println!("\n7. Validation error (invalid email)");
359 let resp = client
360 .post("/users")
361 .header("content-type", "application/json")
362 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
363 .send();
364 println!(
365 " POST /users -> {} {}",
366 resp.status().as_u16(),
367 resp.text()
368 );
369 assert_eq!(resp.status().as_u16(), 400);
370
371 // 8. Wrong Content-Type
372 println!("\n8. Wrong Content-Type");
373 let resp = client
374 .post("/users")
375 .header("content-type", "text/plain")
376 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
377 .send();
378 println!(
379 " POST /users -> {} {}",
380 resp.status().as_u16(),
381 resp.text()
382 );
383 assert_eq!(resp.status().as_u16(), 415);
384
385 // 9. Delete user
386 println!("\n9. Delete user");
387 let resp = client.delete("/users/2").send();
388 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
389 assert_eq!(resp.status().as_u16(), 204);
390
391 // 10. Verify deletion
392 println!("\n10. Verify deletion");
393 let resp = client.get("/users/2").send();
394 println!(
395 " GET /users/2 -> {} {}",
396 resp.status().as_u16(),
397 resp.text()
398 );
399 assert_eq!(resp.status().as_u16(), 404);
400
401 let resp = client.get("/users").send();
402 println!(
403 " GET /users -> {} {}",
404 resp.status().as_u16(),
405 resp.text()
406 );
407 assert_eq!(resp.status().as_u16(), 200);
408
409 println!("\nAll CRUD operations passed!");
410}248fn main() {
249 println!("fastapi_rust Authentication Example");
250 println!("====================================\n");
251
252 // Build the application with public and protected routes
253 let app = App::builder()
254 // Public endpoints - accessible to everyone
255 .get("/public", public_handler)
256 // Login endpoint - returns a token
257 .post("/login", login_handler)
258 // Protected endpoint - requires valid bearer token
259 .get("/protected", protected_handler)
260 .build();
261
262 println!("App created with {} route(s)\n", app.route_count());
263
264 // Create a test client
265 let client = TestClient::new(app);
266
267 // =========================================================================
268 // Test 1: Public endpoint - no auth required
269 // =========================================================================
270 println!("1. Public endpoint - no auth required");
271 let response = client.get("/public").send();
272 println!(
273 " GET /public -> {} {}",
274 response.status().as_u16(),
275 response.status().canonical_reason()
276 );
277 assert_eq!(response.status().as_u16(), 200);
278 assert!(response.text().contains("public endpoint"));
279
280 // =========================================================================
281 // Test 2: Protected endpoint - without token (should get 401)
282 // =========================================================================
283 println!("\n2. Protected endpoint - without token");
284 let response = client.get("/protected").send();
285 println!(
286 " GET /protected -> {} {}",
287 response.status().as_u16(),
288 response.status().canonical_reason()
289 );
290 assert_eq!(
291 response.status().as_u16(),
292 401,
293 "Protected endpoint should return 401 without token"
294 );
295
296 // Check for WWW-Authenticate header
297 let has_www_auth = response
298 .headers()
299 .iter()
300 .any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
301 assert!(
302 has_www_auth,
303 "401 response should include WWW-Authenticate: Bearer header"
304 );
305
306 // =========================================================================
307 // Test 3: Login endpoint - get a token
308 // =========================================================================
309 println!("\n3. Login endpoint - get a token");
310 let response = client
311 .post("/login")
312 .header("content-type", "application/json")
313 .body(r#"{"username":"test","password":"test123"}"#)
314 .send();
315 println!(
316 " POST /login -> {} {}",
317 response.status().as_u16(),
318 response.status().canonical_reason()
319 );
320 assert_eq!(response.status().as_u16(), 200);
321
322 // Parse the response to get the token
323 let body: serde_json::Value = serde_json::from_str(response.text()).unwrap();
324 let token = body["access_token"].as_str().unwrap();
325 println!(" Token: {token}");
326 assert_eq!(token, SECRET_TOKEN);
327
328 // =========================================================================
329 // Test 4: Protected endpoint - with valid token (should get 200)
330 // =========================================================================
331 println!("\n4. Protected endpoint - with valid token");
332 let response = client
333 .get("/protected")
334 .header("authorization", format!("Bearer {SECRET_TOKEN}"))
335 .send();
336 println!(
337 " GET /protected (Authorization: Bearer {}) -> {} {}",
338 SECRET_TOKEN,
339 response.status().as_u16(),
340 response.status().canonical_reason()
341 );
342 assert_eq!(
343 response.status().as_u16(),
344 200,
345 "Protected endpoint should return 200 with valid token"
346 );
347 assert!(response.text().contains("protected resource"));
348
349 // =========================================================================
350 // Test 5: Protected endpoint - with invalid token (should get 403)
351 // =========================================================================
352 println!("\n5. Protected endpoint - with invalid token");
353 let response = client
354 .get("/protected")
355 .header("authorization", "Bearer wrong_token")
356 .send();
357 println!(
358 " GET /protected (Authorization: Bearer wrong_token) -> {} {}",
359 response.status().as_u16(),
360 response.status().canonical_reason()
361 );
362 assert_eq!(
363 response.status().as_u16(),
364 403,
365 "Protected endpoint should return 403 with invalid token"
366 );
367
368 // =========================================================================
369 // Test 6: Protected endpoint - with wrong auth scheme (should get 401)
370 // =========================================================================
371 println!("\n6. Protected endpoint - with wrong auth scheme");
372 let response = client
373 .get("/protected")
374 .header("authorization", "Basic dXNlcjpwYXNz")
375 .send();
376 println!(
377 " GET /protected (Authorization: Basic ...) -> {} {}",
378 response.status().as_u16(),
379 response.status().canonical_reason()
380 );
381 assert_eq!(
382 response.status().as_u16(),
383 401,
384 "Protected endpoint should return 401 with wrong auth scheme"
385 );
386
387 // =========================================================================
388 // Test 7: Login with wrong Content-Type (should get 415)
389 // =========================================================================
390 println!("\n7. Login with wrong Content-Type");
391 let response = client
392 .post("/login")
393 .header("content-type", "text/plain")
394 .body("username=test&password=test123")
395 .send();
396 println!(
397 " POST /login (Content-Type: text/plain) -> {} {}",
398 response.status().as_u16(),
399 response.status().canonical_reason()
400 );
401 assert_eq!(
402 response.status().as_u16(),
403 415,
404 "Login should return 415 with wrong Content-Type"
405 );
406
407 // =========================================================================
408 // Test 8: Token case sensitivity (lowercase 'bearer')
409 // =========================================================================
410 println!("\n8. Token case sensitivity (lowercase 'bearer')");
411 let response = client
412 .get("/protected")
413 .header("authorization", format!("bearer {SECRET_TOKEN}"))
414 .send();
415 println!(
416 " GET /protected (Authorization: bearer {}) -> {} {}",
417 SECRET_TOKEN,
418 response.status().as_u16(),
419 response.status().canonical_reason()
420 );
421 assert_eq!(
422 response.status().as_u16(),
423 200,
424 "Bearer scheme should be case-insensitive (lowercase accepted)"
425 );
426
427 println!("\nAll authentication tests passed!");
428}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?
252fn main() {
253 // Initialize the global store
254 *STORE.lock().unwrap() = Some(UserDb {
255 users: HashMap::new(),
256 next_id: 1,
257 });
258
259 println!("fastapi_rust CRUD API Example");
260 println!("=============================\n");
261
262 let app = App::builder()
263 .post("/users", create_user)
264 .get("/users", list_users)
265 .get("/users/{id}", get_user)
266 .put("/users/{id}", update_user)
267 .delete("/users/{id}", delete_user)
268 .build();
269
270 println!("App created with {} route(s)\n", app.route_count());
271 let client = TestClient::new(app);
272
273 // 1. Create users
274 println!("1. Create users");
275 let resp = client
276 .post("/users")
277 .header("content-type", "application/json")
278 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
279 .send();
280 println!(
281 " POST /users -> {} {}",
282 resp.status().as_u16(),
283 resp.text()
284 );
285 assert_eq!(resp.status().as_u16(), 201);
286
287 let resp = client
288 .post("/users")
289 .header("content-type", "application/json")
290 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
291 .send();
292 println!(
293 " POST /users -> {} {}",
294 resp.status().as_u16(),
295 resp.text()
296 );
297 assert_eq!(resp.status().as_u16(), 201);
298
299 // 2. List users
300 println!("\n2. List all users");
301 let resp = client.get("/users").send();
302 println!(
303 " GET /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 assert_eq!(resp.status().as_u16(), 200);
308
309 // 3. Get user by ID
310 println!("\n3. Get user by ID");
311 let resp = client.get("/users/1").send();
312 println!(
313 " GET /users/1 -> {} {}",
314 resp.status().as_u16(),
315 resp.text()
316 );
317 assert_eq!(resp.status().as_u16(), 200);
318
319 // 4. Get nonexistent user
320 println!("\n4. Get nonexistent user");
321 let resp = client.get("/users/999").send();
322 println!(
323 " GET /users/999 -> {} {}",
324 resp.status().as_u16(),
325 resp.text()
326 );
327 assert_eq!(resp.status().as_u16(), 404);
328
329 // 5. Update user
330 println!("\n5. Update user");
331 let resp = client
332 .put("/users/1")
333 .header("content-type", "application/json")
334 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
335 .send();
336 println!(
337 " PUT /users/1 -> {} {}",
338 resp.status().as_u16(),
339 resp.text()
340 );
341 assert_eq!(resp.status().as_u16(), 200);
342
343 // 6. Validation: empty name
344 println!("\n6. Validation error (empty name)");
345 let resp = client
346 .post("/users")
347 .header("content-type", "application/json")
348 .body(r#"{"name": "", "email": "bad@example.com"}"#)
349 .send();
350 println!(
351 " POST /users -> {} {}",
352 resp.status().as_u16(),
353 resp.text()
354 );
355 assert_eq!(resp.status().as_u16(), 400);
356
357 // 7. Validation: invalid email
358 println!("\n7. Validation error (invalid email)");
359 let resp = client
360 .post("/users")
361 .header("content-type", "application/json")
362 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
363 .send();
364 println!(
365 " POST /users -> {} {}",
366 resp.status().as_u16(),
367 resp.text()
368 );
369 assert_eq!(resp.status().as_u16(), 400);
370
371 // 8. Wrong Content-Type
372 println!("\n8. Wrong Content-Type");
373 let resp = client
374 .post("/users")
375 .header("content-type", "text/plain")
376 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
377 .send();
378 println!(
379 " POST /users -> {} {}",
380 resp.status().as_u16(),
381 resp.text()
382 );
383 assert_eq!(resp.status().as_u16(), 415);
384
385 // 9. Delete user
386 println!("\n9. Delete user");
387 let resp = client.delete("/users/2").send();
388 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
389 assert_eq!(resp.status().as_u16(), 204);
390
391 // 10. Verify deletion
392 println!("\n10. Verify deletion");
393 let resp = client.get("/users/2").send();
394 println!(
395 " GET /users/2 -> {} {}",
396 resp.status().as_u16(),
397 resp.text()
398 );
399 assert_eq!(resp.status().as_u16(), 404);
400
401 let resp = client.get("/users").send();
402 println!(
403 " GET /users -> {} {}",
404 resp.status().as_u16(),
405 resp.text()
406 );
407 assert_eq!(resp.status().as_u16(), 200);
408
409 println!("\nAll CRUD operations passed!");
410}More examples
248fn main() {
249 println!("fastapi_rust Authentication Example");
250 println!("====================================\n");
251
252 // Build the application with public and protected routes
253 let app = App::builder()
254 // Public endpoints - accessible to everyone
255 .get("/public", public_handler)
256 // Login endpoint - returns a token
257 .post("/login", login_handler)
258 // Protected endpoint - requires valid bearer token
259 .get("/protected", protected_handler)
260 .build();
261
262 println!("App created with {} route(s)\n", app.route_count());
263
264 // Create a test client
265 let client = TestClient::new(app);
266
267 // =========================================================================
268 // Test 1: Public endpoint - no auth required
269 // =========================================================================
270 println!("1. Public endpoint - no auth required");
271 let response = client.get("/public").send();
272 println!(
273 " GET /public -> {} {}",
274 response.status().as_u16(),
275 response.status().canonical_reason()
276 );
277 assert_eq!(response.status().as_u16(), 200);
278 assert!(response.text().contains("public endpoint"));
279
280 // =========================================================================
281 // Test 2: Protected endpoint - without token (should get 401)
282 // =========================================================================
283 println!("\n2. Protected endpoint - without token");
284 let response = client.get("/protected").send();
285 println!(
286 " GET /protected -> {} {}",
287 response.status().as_u16(),
288 response.status().canonical_reason()
289 );
290 assert_eq!(
291 response.status().as_u16(),
292 401,
293 "Protected endpoint should return 401 without token"
294 );
295
296 // Check for WWW-Authenticate header
297 let has_www_auth = response
298 .headers()
299 .iter()
300 .any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
301 assert!(
302 has_www_auth,
303 "401 response should include WWW-Authenticate: Bearer header"
304 );
305
306 // =========================================================================
307 // Test 3: Login endpoint - get a token
308 // =========================================================================
309 println!("\n3. Login endpoint - get a token");
310 let response = client
311 .post("/login")
312 .header("content-type", "application/json")
313 .body(r#"{"username":"test","password":"test123"}"#)
314 .send();
315 println!(
316 " POST /login -> {} {}",
317 response.status().as_u16(),
318 response.status().canonical_reason()
319 );
320 assert_eq!(response.status().as_u16(), 200);
321
322 // Parse the response to get the token
323 let body: serde_json::Value = serde_json::from_str(response.text()).unwrap();
324 let token = body["access_token"].as_str().unwrap();
325 println!(" Token: {token}");
326 assert_eq!(token, SECRET_TOKEN);
327
328 // =========================================================================
329 // Test 4: Protected endpoint - with valid token (should get 200)
330 // =========================================================================
331 println!("\n4. Protected endpoint - with valid token");
332 let response = client
333 .get("/protected")
334 .header("authorization", format!("Bearer {SECRET_TOKEN}"))
335 .send();
336 println!(
337 " GET /protected (Authorization: Bearer {}) -> {} {}",
338 SECRET_TOKEN,
339 response.status().as_u16(),
340 response.status().canonical_reason()
341 );
342 assert_eq!(
343 response.status().as_u16(),
344 200,
345 "Protected endpoint should return 200 with valid token"
346 );
347 assert!(response.text().contains("protected resource"));
348
349 // =========================================================================
350 // Test 5: Protected endpoint - with invalid token (should get 403)
351 // =========================================================================
352 println!("\n5. Protected endpoint - with invalid token");
353 let response = client
354 .get("/protected")
355 .header("authorization", "Bearer wrong_token")
356 .send();
357 println!(
358 " GET /protected (Authorization: Bearer wrong_token) -> {} {}",
359 response.status().as_u16(),
360 response.status().canonical_reason()
361 );
362 assert_eq!(
363 response.status().as_u16(),
364 403,
365 "Protected endpoint should return 403 with invalid token"
366 );
367
368 // =========================================================================
369 // Test 6: Protected endpoint - with wrong auth scheme (should get 401)
370 // =========================================================================
371 println!("\n6. Protected endpoint - with wrong auth scheme");
372 let response = client
373 .get("/protected")
374 .header("authorization", "Basic dXNlcjpwYXNz")
375 .send();
376 println!(
377 " GET /protected (Authorization: Basic ...) -> {} {}",
378 response.status().as_u16(),
379 response.status().canonical_reason()
380 );
381 assert_eq!(
382 response.status().as_u16(),
383 401,
384 "Protected endpoint should return 401 with wrong auth scheme"
385 );
386
387 // =========================================================================
388 // Test 7: Login with wrong Content-Type (should get 415)
389 // =========================================================================
390 println!("\n7. Login with wrong Content-Type");
391 let response = client
392 .post("/login")
393 .header("content-type", "text/plain")
394 .body("username=test&password=test123")
395 .send();
396 println!(
397 " POST /login (Content-Type: text/plain) -> {} {}",
398 response.status().as_u16(),
399 response.status().canonical_reason()
400 );
401 assert_eq!(
402 response.status().as_u16(),
403 415,
404 "Login should return 415 with wrong Content-Type"
405 );
406
407 // =========================================================================
408 // Test 8: Token case sensitivity (lowercase 'bearer')
409 // =========================================================================
410 println!("\n8. Token case sensitivity (lowercase 'bearer')");
411 let response = client
412 .get("/protected")
413 .header("authorization", format!("bearer {SECRET_TOKEN}"))
414 .send();
415 println!(
416 " GET /protected (Authorization: bearer {}) -> {} {}",
417 SECRET_TOKEN,
418 response.status().as_u16(),
419 response.status().canonical_reason()
420 );
421 assert_eq!(
422 response.status().as_u16(),
423 200,
424 "Bearer scheme should be case-insensitive (lowercase accepted)"
425 );
426
427 println!("\nAll authentication tests passed!");
428}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?
252fn main() {
253 // Initialize the global store
254 *STORE.lock().unwrap() = Some(UserDb {
255 users: HashMap::new(),
256 next_id: 1,
257 });
258
259 println!("fastapi_rust CRUD API Example");
260 println!("=============================\n");
261
262 let app = App::builder()
263 .post("/users", create_user)
264 .get("/users", list_users)
265 .get("/users/{id}", get_user)
266 .put("/users/{id}", update_user)
267 .delete("/users/{id}", delete_user)
268 .build();
269
270 println!("App created with {} route(s)\n", app.route_count());
271 let client = TestClient::new(app);
272
273 // 1. Create users
274 println!("1. Create users");
275 let resp = client
276 .post("/users")
277 .header("content-type", "application/json")
278 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
279 .send();
280 println!(
281 " POST /users -> {} {}",
282 resp.status().as_u16(),
283 resp.text()
284 );
285 assert_eq!(resp.status().as_u16(), 201);
286
287 let resp = client
288 .post("/users")
289 .header("content-type", "application/json")
290 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
291 .send();
292 println!(
293 " POST /users -> {} {}",
294 resp.status().as_u16(),
295 resp.text()
296 );
297 assert_eq!(resp.status().as_u16(), 201);
298
299 // 2. List users
300 println!("\n2. List all users");
301 let resp = client.get("/users").send();
302 println!(
303 " GET /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 assert_eq!(resp.status().as_u16(), 200);
308
309 // 3. Get user by ID
310 println!("\n3. Get user by ID");
311 let resp = client.get("/users/1").send();
312 println!(
313 " GET /users/1 -> {} {}",
314 resp.status().as_u16(),
315 resp.text()
316 );
317 assert_eq!(resp.status().as_u16(), 200);
318
319 // 4. Get nonexistent user
320 println!("\n4. Get nonexistent user");
321 let resp = client.get("/users/999").send();
322 println!(
323 " GET /users/999 -> {} {}",
324 resp.status().as_u16(),
325 resp.text()
326 );
327 assert_eq!(resp.status().as_u16(), 404);
328
329 // 5. Update user
330 println!("\n5. Update user");
331 let resp = client
332 .put("/users/1")
333 .header("content-type", "application/json")
334 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
335 .send();
336 println!(
337 " PUT /users/1 -> {} {}",
338 resp.status().as_u16(),
339 resp.text()
340 );
341 assert_eq!(resp.status().as_u16(), 200);
342
343 // 6. Validation: empty name
344 println!("\n6. Validation error (empty name)");
345 let resp = client
346 .post("/users")
347 .header("content-type", "application/json")
348 .body(r#"{"name": "", "email": "bad@example.com"}"#)
349 .send();
350 println!(
351 " POST /users -> {} {}",
352 resp.status().as_u16(),
353 resp.text()
354 );
355 assert_eq!(resp.status().as_u16(), 400);
356
357 // 7. Validation: invalid email
358 println!("\n7. Validation error (invalid email)");
359 let resp = client
360 .post("/users")
361 .header("content-type", "application/json")
362 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
363 .send();
364 println!(
365 " POST /users -> {} {}",
366 resp.status().as_u16(),
367 resp.text()
368 );
369 assert_eq!(resp.status().as_u16(), 400);
370
371 // 8. Wrong Content-Type
372 println!("\n8. Wrong Content-Type");
373 let resp = client
374 .post("/users")
375 .header("content-type", "text/plain")
376 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
377 .send();
378 println!(
379 " POST /users -> {} {}",
380 resp.status().as_u16(),
381 resp.text()
382 );
383 assert_eq!(resp.status().as_u16(), 415);
384
385 // 9. Delete user
386 println!("\n9. Delete user");
387 let resp = client.delete("/users/2").send();
388 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
389 assert_eq!(resp.status().as_u16(), 204);
390
391 // 10. Verify deletion
392 println!("\n10. Verify deletion");
393 let resp = client.get("/users/2").send();
394 println!(
395 " GET /users/2 -> {} {}",
396 resp.status().as_u16(),
397 resp.text()
398 );
399 assert_eq!(resp.status().as_u16(), 404);
400
401 let resp = client.get("/users").send();
402 println!(
403 " GET /users -> {} {}",
404 resp.status().as_u16(),
405 resp.text()
406 );
407 assert_eq!(resp.status().as_u16(), 200);
408
409 println!("\nAll CRUD operations passed!");
410}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?
252fn main() {
253 // Initialize the global store
254 *STORE.lock().unwrap() = Some(UserDb {
255 users: HashMap::new(),
256 next_id: 1,
257 });
258
259 println!("fastapi_rust CRUD API Example");
260 println!("=============================\n");
261
262 let app = App::builder()
263 .post("/users", create_user)
264 .get("/users", list_users)
265 .get("/users/{id}", get_user)
266 .put("/users/{id}", update_user)
267 .delete("/users/{id}", delete_user)
268 .build();
269
270 println!("App created with {} route(s)\n", app.route_count());
271 let client = TestClient::new(app);
272
273 // 1. Create users
274 println!("1. Create users");
275 let resp = client
276 .post("/users")
277 .header("content-type", "application/json")
278 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
279 .send();
280 println!(
281 " POST /users -> {} {}",
282 resp.status().as_u16(),
283 resp.text()
284 );
285 assert_eq!(resp.status().as_u16(), 201);
286
287 let resp = client
288 .post("/users")
289 .header("content-type", "application/json")
290 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
291 .send();
292 println!(
293 " POST /users -> {} {}",
294 resp.status().as_u16(),
295 resp.text()
296 );
297 assert_eq!(resp.status().as_u16(), 201);
298
299 // 2. List users
300 println!("\n2. List all users");
301 let resp = client.get("/users").send();
302 println!(
303 " GET /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 assert_eq!(resp.status().as_u16(), 200);
308
309 // 3. Get user by ID
310 println!("\n3. Get user by ID");
311 let resp = client.get("/users/1").send();
312 println!(
313 " GET /users/1 -> {} {}",
314 resp.status().as_u16(),
315 resp.text()
316 );
317 assert_eq!(resp.status().as_u16(), 200);
318
319 // 4. Get nonexistent user
320 println!("\n4. Get nonexistent user");
321 let resp = client.get("/users/999").send();
322 println!(
323 " GET /users/999 -> {} {}",
324 resp.status().as_u16(),
325 resp.text()
326 );
327 assert_eq!(resp.status().as_u16(), 404);
328
329 // 5. Update user
330 println!("\n5. Update user");
331 let resp = client
332 .put("/users/1")
333 .header("content-type", "application/json")
334 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
335 .send();
336 println!(
337 " PUT /users/1 -> {} {}",
338 resp.status().as_u16(),
339 resp.text()
340 );
341 assert_eq!(resp.status().as_u16(), 200);
342
343 // 6. Validation: empty name
344 println!("\n6. Validation error (empty name)");
345 let resp = client
346 .post("/users")
347 .header("content-type", "application/json")
348 .body(r#"{"name": "", "email": "bad@example.com"}"#)
349 .send();
350 println!(
351 " POST /users -> {} {}",
352 resp.status().as_u16(),
353 resp.text()
354 );
355 assert_eq!(resp.status().as_u16(), 400);
356
357 // 7. Validation: invalid email
358 println!("\n7. Validation error (invalid email)");
359 let resp = client
360 .post("/users")
361 .header("content-type", "application/json")
362 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
363 .send();
364 println!(
365 " POST /users -> {} {}",
366 resp.status().as_u16(),
367 resp.text()
368 );
369 assert_eq!(resp.status().as_u16(), 400);
370
371 // 8. Wrong Content-Type
372 println!("\n8. Wrong Content-Type");
373 let resp = client
374 .post("/users")
375 .header("content-type", "text/plain")
376 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
377 .send();
378 println!(
379 " POST /users -> {} {}",
380 resp.status().as_u16(),
381 resp.text()
382 );
383 assert_eq!(resp.status().as_u16(), 415);
384
385 // 9. Delete user
386 println!("\n9. Delete user");
387 let resp = client.delete("/users/2").send();
388 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
389 assert_eq!(resp.status().as_u16(), 204);
390
391 // 10. Verify deletion
392 println!("\n10. Verify deletion");
393 let resp = client.get("/users/2").send();
394 println!(
395 " GET /users/2 -> {} {}",
396 resp.status().as_u16(),
397 resp.text()
398 );
399 assert_eq!(resp.status().as_u16(), 404);
400
401 let resp = client.get("/users").send();
402 println!(
403 " GET /users -> {} {}",
404 resp.status().as_u16(),
405 resp.text()
406 );
407 assert_eq!(resp.status().as_u16(), 200);
408
409 println!("\nAll CRUD operations passed!");
410}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?
24fn main() {
25 println!("Getting Started Guide - Code Validation\n");
26
27 // === Basic App Example ===
28 println!("1. Basic app with two routes:");
29 let app = App::builder()
30 .get("/", hello)
31 .get("/health", health)
32 .build();
33
34 println!(" Routes: {}", app.route_count());
35 let client = TestClient::new(app);
36
37 let response = client.get("/").send();
38 println!(
39 " GET / -> {} ({})",
40 response.status().as_u16(),
41 response.text()
42 );
43 assert_eq!(response.status().as_u16(), 200);
44 assert_eq!(response.text(), "Hello, World!");
45
46 let response = client.get("/health").send();
47 println!(
48 " GET /health -> {} ({})",
49 response.status().as_u16(),
50 response.text()
51 );
52 assert_eq!(response.status().as_u16(), 200);
53
54 // === App with Middleware ===
55 println!("\n2. App with middleware:");
56 let app = App::builder()
57 .middleware(RequestIdMiddleware::new())
58 .middleware(SecurityHeaders::new())
59 .get("/", hello)
60 .build();
61
62 let client = TestClient::new(app);
63 let response = client.get("/").send();
64 println!(" GET / -> {}", response.status().as_u16());
65 assert_eq!(response.status().as_u16(), 200);
66
67 // === App with Configuration ===
68 println!("\n3. App with configuration:");
69 let config = AppConfig::new()
70 .name("My API")
71 .version("1.0.0")
72 .debug(true)
73 .max_body_size(10 * 1024 * 1024)
74 .request_timeout_ms(30_000);
75
76 let app = App::builder().config(config).get("/", hello).build();
77
78 println!(" App name: {}", app.config().name);
79 println!(" Version: {}", app.config().version);
80 assert_eq!(app.config().name, "My API");
81 assert_eq!(app.config().version, "1.0.0");
82
83 // === 404 for unknown routes ===
84 println!("\n4. 404 for unknown routes:");
85 let app = App::builder().get("/", hello).build();
86
87 let client = TestClient::new(app);
88 let response = client.get("/nonexistent").send();
89 println!(" GET /nonexistent -> {}", response.status().as_u16());
90 assert_eq!(response.status().as_u16(), 404);
91
92 println!("\nAll getting started examples validated successfully!");
93}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?
51fn main() {
52 println!("fastapi_rust Hello World Example");
53 println!("================================\n");
54
55 // Build the application
56 //
57 // App::builder() creates a new application builder that lets you:
58 // - Add routes for different HTTP methods
59 // - Configure middleware
60 // - Set application state
61 // - Define exception handlers
62 let app = App::builder()
63 // Register a GET handler for the root path "/"
64 //
65 // This is equivalent to:
66 // @app.get("/")
67 // def hello():
68 // return "Hello, World!"
69 // in Python FastAPI
70 .get("/", hello_handler)
71 // Build the final immutable App
72 .build();
73
74 println!("App created with {} route(s)\n", app.route_count());
75
76 // Create a test client to make requests to our app
77 //
78 // TestClient wraps any Handler (including App) and provides
79 // a convenient API for making HTTP requests in tests.
80 let client = TestClient::new(app);
81
82 // Make a GET request to "/"
83 println!("Making request: GET /");
84 let response = client.get("/").send();
85
86 // Check the response
87 println!(
88 "GET / -> {} {}",
89 response.status().as_u16(),
90 response.status().canonical_reason()
91 );
92 println!("Response: {}\n", response.text());
93
94 // Verify success
95 assert_eq!(response.status().as_u16(), 200);
96 assert_eq!(response.text(), "Hello, World!");
97
98 // Try a path that doesn't exist - should get 404
99 println!("Making request: GET /not-found");
100 let response = client.get("/not-found").send();
101 println!(
102 "GET /not-found -> {} {}",
103 response.status().as_u16(),
104 response.status().canonical_reason()
105 );
106 assert_eq!(response.status().as_u16(), 404);
107
108 println!("\nAll assertions passed!");
109}More examples
24fn main() {
25 println!("Getting Started Guide - Code Validation\n");
26
27 // === Basic App Example ===
28 println!("1. Basic app with two routes:");
29 let app = App::builder()
30 .get("/", hello)
31 .get("/health", health)
32 .build();
33
34 println!(" Routes: {}", app.route_count());
35 let client = TestClient::new(app);
36
37 let response = client.get("/").send();
38 println!(
39 " GET / -> {} ({})",
40 response.status().as_u16(),
41 response.text()
42 );
43 assert_eq!(response.status().as_u16(), 200);
44 assert_eq!(response.text(), "Hello, World!");
45
46 let response = client.get("/health").send();
47 println!(
48 " GET /health -> {} ({})",
49 response.status().as_u16(),
50 response.text()
51 );
52 assert_eq!(response.status().as_u16(), 200);
53
54 // === App with Middleware ===
55 println!("\n2. App with middleware:");
56 let app = App::builder()
57 .middleware(RequestIdMiddleware::new())
58 .middleware(SecurityHeaders::new())
59 .get("/", hello)
60 .build();
61
62 let client = TestClient::new(app);
63 let response = client.get("/").send();
64 println!(" GET / -> {}", response.status().as_u16());
65 assert_eq!(response.status().as_u16(), 200);
66
67 // === App with Configuration ===
68 println!("\n3. App with configuration:");
69 let config = AppConfig::new()
70 .name("My API")
71 .version("1.0.0")
72 .debug(true)
73 .max_body_size(10 * 1024 * 1024)
74 .request_timeout_ms(30_000);
75
76 let app = App::builder().config(config).get("/", hello).build();
77
78 println!(" App name: {}", app.config().name);
79 println!(" Version: {}", app.config().version);
80 assert_eq!(app.config().name, "My API");
81 assert_eq!(app.config().version, "1.0.0");
82
83 // === 404 for unknown routes ===
84 println!("\n4. 404 for unknown routes:");
85 let app = App::builder().get("/", hello).build();
86
87 let client = TestClient::new(app);
88 let response = client.get("/nonexistent").send();
89 println!(" GET /nonexistent -> {}", response.status().as_u16());
90 assert_eq!(response.status().as_u16(), 404);
91
92 println!("\nAll getting started examples validated successfully!");
93}252fn main() {
253 // Initialize the global store
254 *STORE.lock().unwrap() = Some(UserDb {
255 users: HashMap::new(),
256 next_id: 1,
257 });
258
259 println!("fastapi_rust CRUD API Example");
260 println!("=============================\n");
261
262 let app = App::builder()
263 .post("/users", create_user)
264 .get("/users", list_users)
265 .get("/users/{id}", get_user)
266 .put("/users/{id}", update_user)
267 .delete("/users/{id}", delete_user)
268 .build();
269
270 println!("App created with {} route(s)\n", app.route_count());
271 let client = TestClient::new(app);
272
273 // 1. Create users
274 println!("1. Create users");
275 let resp = client
276 .post("/users")
277 .header("content-type", "application/json")
278 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
279 .send();
280 println!(
281 " POST /users -> {} {}",
282 resp.status().as_u16(),
283 resp.text()
284 );
285 assert_eq!(resp.status().as_u16(), 201);
286
287 let resp = client
288 .post("/users")
289 .header("content-type", "application/json")
290 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
291 .send();
292 println!(
293 " POST /users -> {} {}",
294 resp.status().as_u16(),
295 resp.text()
296 );
297 assert_eq!(resp.status().as_u16(), 201);
298
299 // 2. List users
300 println!("\n2. List all users");
301 let resp = client.get("/users").send();
302 println!(
303 " GET /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 assert_eq!(resp.status().as_u16(), 200);
308
309 // 3. Get user by ID
310 println!("\n3. Get user by ID");
311 let resp = client.get("/users/1").send();
312 println!(
313 " GET /users/1 -> {} {}",
314 resp.status().as_u16(),
315 resp.text()
316 );
317 assert_eq!(resp.status().as_u16(), 200);
318
319 // 4. Get nonexistent user
320 println!("\n4. Get nonexistent user");
321 let resp = client.get("/users/999").send();
322 println!(
323 " GET /users/999 -> {} {}",
324 resp.status().as_u16(),
325 resp.text()
326 );
327 assert_eq!(resp.status().as_u16(), 404);
328
329 // 5. Update user
330 println!("\n5. Update user");
331 let resp = client
332 .put("/users/1")
333 .header("content-type", "application/json")
334 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
335 .send();
336 println!(
337 " PUT /users/1 -> {} {}",
338 resp.status().as_u16(),
339 resp.text()
340 );
341 assert_eq!(resp.status().as_u16(), 200);
342
343 // 6. Validation: empty name
344 println!("\n6. Validation error (empty name)");
345 let resp = client
346 .post("/users")
347 .header("content-type", "application/json")
348 .body(r#"{"name": "", "email": "bad@example.com"}"#)
349 .send();
350 println!(
351 " POST /users -> {} {}",
352 resp.status().as_u16(),
353 resp.text()
354 );
355 assert_eq!(resp.status().as_u16(), 400);
356
357 // 7. Validation: invalid email
358 println!("\n7. Validation error (invalid email)");
359 let resp = client
360 .post("/users")
361 .header("content-type", "application/json")
362 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
363 .send();
364 println!(
365 " POST /users -> {} {}",
366 resp.status().as_u16(),
367 resp.text()
368 );
369 assert_eq!(resp.status().as_u16(), 400);
370
371 // 8. Wrong Content-Type
372 println!("\n8. Wrong Content-Type");
373 let resp = client
374 .post("/users")
375 .header("content-type", "text/plain")
376 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
377 .send();
378 println!(
379 " POST /users -> {} {}",
380 resp.status().as_u16(),
381 resp.text()
382 );
383 assert_eq!(resp.status().as_u16(), 415);
384
385 // 9. Delete user
386 println!("\n9. Delete user");
387 let resp = client.delete("/users/2").send();
388 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
389 assert_eq!(resp.status().as_u16(), 204);
390
391 // 10. Verify deletion
392 println!("\n10. Verify deletion");
393 let resp = client.get("/users/2").send();
394 println!(
395 " GET /users/2 -> {} {}",
396 resp.status().as_u16(),
397 resp.text()
398 );
399 assert_eq!(resp.status().as_u16(), 404);
400
401 let resp = client.get("/users").send();
402 println!(
403 " GET /users -> {} {}",
404 resp.status().as_u16(),
405 resp.text()
406 );
407 assert_eq!(resp.status().as_u16(), 200);
408
409 println!("\nAll CRUD operations passed!");
410}248fn main() {
249 println!("fastapi_rust Authentication Example");
250 println!("====================================\n");
251
252 // Build the application with public and protected routes
253 let app = App::builder()
254 // Public endpoints - accessible to everyone
255 .get("/public", public_handler)
256 // Login endpoint - returns a token
257 .post("/login", login_handler)
258 // Protected endpoint - requires valid bearer token
259 .get("/protected", protected_handler)
260 .build();
261
262 println!("App created with {} route(s)\n", app.route_count());
263
264 // Create a test client
265 let client = TestClient::new(app);
266
267 // =========================================================================
268 // Test 1: Public endpoint - no auth required
269 // =========================================================================
270 println!("1. Public endpoint - no auth required");
271 let response = client.get("/public").send();
272 println!(
273 " GET /public -> {} {}",
274 response.status().as_u16(),
275 response.status().canonical_reason()
276 );
277 assert_eq!(response.status().as_u16(), 200);
278 assert!(response.text().contains("public endpoint"));
279
280 // =========================================================================
281 // Test 2: Protected endpoint - without token (should get 401)
282 // =========================================================================
283 println!("\n2. Protected endpoint - without token");
284 let response = client.get("/protected").send();
285 println!(
286 " GET /protected -> {} {}",
287 response.status().as_u16(),
288 response.status().canonical_reason()
289 );
290 assert_eq!(
291 response.status().as_u16(),
292 401,
293 "Protected endpoint should return 401 without token"
294 );
295
296 // Check for WWW-Authenticate header
297 let has_www_auth = response
298 .headers()
299 .iter()
300 .any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
301 assert!(
302 has_www_auth,
303 "401 response should include WWW-Authenticate: Bearer header"
304 );
305
306 // =========================================================================
307 // Test 3: Login endpoint - get a token
308 // =========================================================================
309 println!("\n3. Login endpoint - get a token");
310 let response = client
311 .post("/login")
312 .header("content-type", "application/json")
313 .body(r#"{"username":"test","password":"test123"}"#)
314 .send();
315 println!(
316 " POST /login -> {} {}",
317 response.status().as_u16(),
318 response.status().canonical_reason()
319 );
320 assert_eq!(response.status().as_u16(), 200);
321
322 // Parse the response to get the token
323 let body: serde_json::Value = serde_json::from_str(response.text()).unwrap();
324 let token = body["access_token"].as_str().unwrap();
325 println!(" Token: {token}");
326 assert_eq!(token, SECRET_TOKEN);
327
328 // =========================================================================
329 // Test 4: Protected endpoint - with valid token (should get 200)
330 // =========================================================================
331 println!("\n4. Protected endpoint - with valid token");
332 let response = client
333 .get("/protected")
334 .header("authorization", format!("Bearer {SECRET_TOKEN}"))
335 .send();
336 println!(
337 " GET /protected (Authorization: Bearer {}) -> {} {}",
338 SECRET_TOKEN,
339 response.status().as_u16(),
340 response.status().canonical_reason()
341 );
342 assert_eq!(
343 response.status().as_u16(),
344 200,
345 "Protected endpoint should return 200 with valid token"
346 );
347 assert!(response.text().contains("protected resource"));
348
349 // =========================================================================
350 // Test 5: Protected endpoint - with invalid token (should get 403)
351 // =========================================================================
352 println!("\n5. Protected endpoint - with invalid token");
353 let response = client
354 .get("/protected")
355 .header("authorization", "Bearer wrong_token")
356 .send();
357 println!(
358 " GET /protected (Authorization: Bearer wrong_token) -> {} {}",
359 response.status().as_u16(),
360 response.status().canonical_reason()
361 );
362 assert_eq!(
363 response.status().as_u16(),
364 403,
365 "Protected endpoint should return 403 with invalid token"
366 );
367
368 // =========================================================================
369 // Test 6: Protected endpoint - with wrong auth scheme (should get 401)
370 // =========================================================================
371 println!("\n6. Protected endpoint - with wrong auth scheme");
372 let response = client
373 .get("/protected")
374 .header("authorization", "Basic dXNlcjpwYXNz")
375 .send();
376 println!(
377 " GET /protected (Authorization: Basic ...) -> {} {}",
378 response.status().as_u16(),
379 response.status().canonical_reason()
380 );
381 assert_eq!(
382 response.status().as_u16(),
383 401,
384 "Protected endpoint should return 401 with wrong auth scheme"
385 );
386
387 // =========================================================================
388 // Test 7: Login with wrong Content-Type (should get 415)
389 // =========================================================================
390 println!("\n7. Login with wrong Content-Type");
391 let response = client
392 .post("/login")
393 .header("content-type", "text/plain")
394 .body("username=test&password=test123")
395 .send();
396 println!(
397 " POST /login (Content-Type: text/plain) -> {} {}",
398 response.status().as_u16(),
399 response.status().canonical_reason()
400 );
401 assert_eq!(
402 response.status().as_u16(),
403 415,
404 "Login should return 415 with wrong Content-Type"
405 );
406
407 // =========================================================================
408 // Test 8: Token case sensitivity (lowercase 'bearer')
409 // =========================================================================
410 println!("\n8. Token case sensitivity (lowercase 'bearer')");
411 let response = client
412 .get("/protected")
413 .header("authorization", format!("bearer {SECRET_TOKEN}"))
414 .send();
415 println!(
416 " GET /protected (Authorization: bearer {}) -> {} {}",
417 SECRET_TOKEN,
418 response.status().as_u16(),
419 response.status().canonical_reason()
420 );
421 assert_eq!(
422 response.status().as_u16(),
423 200,
424 "Bearer scheme should be case-insensitive (lowercase accepted)"
425 );
426
427 println!("\nAll authentication tests passed!");
428}