Skip to main content

AppBuilder

Struct AppBuilder 

Source
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

Source

pub fn new() -> AppBuilder

Creates a new application builder with no registered state.

Source§

impl<S> AppBuilder<S>
where S: StateRegistry,

Source

pub fn config(self, config: AppConfig) -> AppBuilder<S>

Sets the application configuration.

Examples found in repository?
examples/getting_started.rs (line 76)
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}
Source

pub fn route<H, Fut>( self, path: impl Into<String>, method: Method, handler: H, ) -> AppBuilder<S>
where H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static, Fut: Future<Output = Response> + Send + 'static,

Adds a route to the application.

Routes are matched in the order they are added.

Source

pub fn get<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
where H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static, Fut: Future<Output = Response> + Send + 'static,

Adds a GET route.

Examples found in repository?
examples/hello_world.rs (line 70)
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
Hide additional examples
examples/getting_started.rs (line 30)
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}
examples/crud_api.rs (line 264)
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}
examples/auth_example.rs (line 255)
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}
Source

pub fn post<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
where H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static, Fut: Future<Output = Response> + Send + 'static,

Adds a POST route.

Examples found in repository?
examples/crud_api.rs (line 263)
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
Hide additional examples
examples/auth_example.rs (line 257)
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}
Source

pub fn put<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
where H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static, Fut: Future<Output = Response> + Send + 'static,

Adds a PUT route.

Examples found in repository?
examples/crud_api.rs (line 266)
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}
Source

pub fn delete<H, Fut>( self, path: impl Into<String>, handler: H, ) -> AppBuilder<S>
where H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static, Fut: Future<Output = Response> + Send + 'static,

Adds a DELETE route.

Examples found in repository?
examples/crud_api.rs (line 267)
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}
Source

pub fn patch<H, Fut>(self, path: impl Into<String>, handler: H) -> AppBuilder<S>
where H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static, Fut: Future<Output = Response> + Send + 'static,

Adds a PATCH route.

Source

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:

  • before hooks run first-to-last
  • after hooks run last-to-first
Examples found in repository?
examples/getting_started.rs (line 57)
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}
Source

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();
Source

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();
Source

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)
Source

pub fn mounted_app_count(&self) -> usize

Returns the number of mounted sub-applications.

Source

pub fn state<T>(self, value: T) -> AppBuilder<S>
where T: Send + Sync + 'static,

👎Deprecated since 0.2.0: Use with_state for compile-time state type verification

Adds 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.

Source

pub fn with_state<T>(self, value: T) -> AppBuilder<(T, S)>
where T: Send + Sync + 'static,

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>) { /* ... */ }
Source

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).

Source

pub fn override_dependency_value<T>(self, value: T) -> AppBuilder<S>
where T: FromDependency,

Registers a fixed dependency override value.

Source

pub fn clear_dependency_overrides(self) -> AppBuilder<S>

Clears all registered dependency overrides.

Source

pub fn exception_handler<E, H>(self, handler: H) -> AppBuilder<S>
where E: Error + Send + Sync + 'static, H: Fn(&RequestContext, E) -> Response + Send + Sync + 'static,

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();
Source

pub fn exception_handlers(self, handlers: ExceptionHandlers) -> AppBuilder<S>

Sets the exception handlers registry.

This replaces any previously registered handlers.

Source

pub fn with_default_exception_handlers(self) -> AppBuilder<S>

Uses default exception handlers for common error types.

This registers handlers for:

Source

pub fn on_startup<F>(self, hook: F) -> AppBuilder<S>
where F: FnOnce() -> Result<(), StartupHookError> + Send + 'static,

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();
Source

pub fn on_startup_async<F, Fut>(self, hook: F) -> AppBuilder<S>
where F: FnOnce() -> Fut + Send + 'static, Fut: Future<Output = Result<(), StartupHookError>> + Send + 'static,

Registers an async startup hook.

Async startup hooks are awaited in registration order.

§Example
let app = App::builder()
    .on_startup_async(|| async {
        let pool = connect_to_database().await?;
        Ok(())
    })
    .build();
Source

pub fn on_shutdown<F>(self, hook: F) -> AppBuilder<S>
where F: FnOnce() + Send + 'static,

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();
Source

pub fn on_shutdown_async<F, Fut>(self, hook: F) -> AppBuilder<S>
where F: FnOnce() -> Fut + Send + 'static, Fut: Future<Output = ()> + Send + 'static,

Registers an async shutdown hook.

Async shutdown hooks are awaited in reverse registration order (LIFO).

§Example
let app = App::builder()
    .on_shutdown_async(|| async {
        flush_metrics().await;
    })
    .build();
Source

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:

  1. Initialize resources (connect to database, start background tasks, etc.)
  2. Return a LifespanScope containing:
    • State to be added to the application (accessible via State<T> extractor)
    • An optional cleanup closure to run during shutdown

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();
Source

pub fn has_lifespan(&self) -> bool

Returns true if a lifespan function has been registered.

Source

pub fn startup_hook_count(&self) -> usize

Returns the number of registered startup hooks.

Source

pub fn shutdown_hook_count(&self) -> usize

Returns the number of registered shutdown hooks.

Source

pub fn build(self) -> App

Builds the application.

This consumes the builder and returns the configured App.

Examples found in repository?
examples/hello_world.rs (line 72)
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
Hide additional examples
examples/getting_started.rs (line 32)
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}
examples/crud_api.rs (line 268)
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}
examples/auth_example.rs (line 260)
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}

Trait Implementations§

Source§

impl<S> Debug for AppBuilder<S>
where S: StateRegistry,

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl Default for AppBuilder

Source§

fn default() -> AppBuilder

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl<S> Freeze for AppBuilder<S>

§

impl<S = ()> !RefUnwindSafe for AppBuilder<S>

§

impl<S> Send for AppBuilder<S>

§

impl<S = ()> !Sync for AppBuilder<S>

§

impl<S> Unpin for AppBuilder<S>
where S: Unpin,

§

impl<S = ()> !UnwindSafe for AppBuilder<S>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, _span: NoopSpan) -> Self

Instruments this future with a span (no-op when disabled).
Source§

fn in_current_span(self) -> Self

Instruments this future with the current span (no-op when disabled).
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> ResponseProduces<T> for T