1use fastapi::core::{
48 App, Body, Request, RequestContext, Response, ResponseBody, StatusCode, TestClient,
49};
50use serde::{Deserialize, Serialize};
51use std::collections::HashMap;
52use std::sync::Mutex;
53
54#[derive(Debug, Clone, Deserialize)]
59struct UserInput {
60 name: String,
61 email: String,
62}
63
64#[derive(Debug, Clone, Serialize)]
65struct User {
66 id: u64,
67 name: String,
68 email: String,
69}
70
71struct UserDb {
72 users: HashMap<u64, User>,
73 next_id: u64,
74}
75
76static STORE: Mutex<Option<UserDb>> = Mutex::new(None);
79
80fn with_db<R>(f: impl FnOnce(&mut UserDb) -> R) -> R {
81 let mut guard = STORE.lock().unwrap();
82 let db = guard.as_mut().expect("store initialized");
83 f(db)
84}
85
86fn parse_json_body<T: serde::de::DeserializeOwned>(req: &mut Request) -> Result<T, Response> {
91 let is_json = req
92 .headers()
93 .get("content-type")
94 .is_some_and(|ct| ct.starts_with(b"application/json"));
95
96 if !is_json {
97 return Err(json_error(
98 StatusCode::UNSUPPORTED_MEDIA_TYPE,
99 "Content-Type must be application/json",
100 ));
101 }
102
103 let Body::Bytes(body) = req.take_body() else {
104 return Err(json_error(StatusCode::BAD_REQUEST, "Missing request body"));
105 };
106 serde_json::from_slice(&body)
107 .map_err(|e| json_error(StatusCode::BAD_REQUEST, &format!("Invalid JSON: {e}")))
108}
109
110fn extract_user_id(req: &Request) -> Result<u64, Response> {
111 let path = req.path();
112 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
113 segments
114 .get(1)
115 .and_then(|s| s.parse::<u64>().ok())
116 .ok_or_else(|| json_error(StatusCode::BAD_REQUEST, "Invalid user ID"))
117}
118
119fn json_error(status: StatusCode, detail: &str) -> Response {
120 let body = serde_json::json!({ "detail": detail });
121 Response::with_status(status)
122 .header("content-type", b"application/json".to_vec())
123 .body(ResponseBody::Bytes(body.to_string().into_bytes()))
124}
125
126fn json_response(status: StatusCode, value: &impl Serialize) -> Response {
127 Response::with_status(status)
128 .header("content-type", b"application/json".to_vec())
129 .body(ResponseBody::Bytes(
130 serde_json::to_string(value).unwrap().into_bytes(),
131 ))
132}
133
134fn validate_input(input: &UserInput) -> Option<Response> {
135 if input.name.trim().is_empty() {
136 return Some(json_error(
137 StatusCode::BAD_REQUEST,
138 "name must not be empty",
139 ));
140 }
141 if !input.email.contains('@') {
142 return Some(json_error(
143 StatusCode::BAD_REQUEST,
144 "email must contain '@'",
145 ));
146 }
147 None
148}
149
150fn create_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
155 let input = match parse_json_body::<UserInput>(req) {
156 Ok(v) => v,
157 Err(r) => return std::future::ready(r),
158 };
159 if let Some(r) = validate_input(&input) {
160 return std::future::ready(r);
161 }
162
163 let user = with_db(|db| {
164 let id = db.next_id;
165 db.next_id += 1;
166 let user = User {
167 id,
168 name: input.name,
169 email: input.email,
170 };
171 db.users.insert(id, user.clone());
172 user
173 });
174
175 std::future::ready(json_response(StatusCode::CREATED, &user))
176}
177
178fn list_users(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
179 let users = with_db(|db| {
180 let mut v: Vec<User> = db.users.values().cloned().collect();
181 v.sort_by_key(|u| u.id);
182 v
183 });
184 std::future::ready(json_response(StatusCode::OK, &users))
185}
186
187fn get_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
188 let id = match extract_user_id(req) {
189 Ok(id) => id,
190 Err(r) => return std::future::ready(r),
191 };
192 let result = with_db(|db| db.users.get(&id).cloned());
193 match result {
194 Some(user) => std::future::ready(json_response(StatusCode::OK, &user)),
195 None => std::future::ready(json_error(
196 StatusCode::NOT_FOUND,
197 &format!("User {id} not found"),
198 )),
199 }
200}
201
202fn update_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
203 let id = match extract_user_id(req) {
204 Ok(id) => id,
205 Err(r) => return std::future::ready(r),
206 };
207 let input = match parse_json_body::<UserInput>(req) {
208 Ok(v) => v,
209 Err(r) => return std::future::ready(r),
210 };
211 if let Some(r) = validate_input(&input) {
212 return std::future::ready(r);
213 }
214
215 let result = with_db(|db| {
216 db.users.get_mut(&id).map(|user| {
217 user.name = input.name;
218 user.email = input.email;
219 user.clone()
220 })
221 });
222 match result {
223 Some(user) => std::future::ready(json_response(StatusCode::OK, &user)),
224 None => std::future::ready(json_error(
225 StatusCode::NOT_FOUND,
226 &format!("User {id} not found"),
227 )),
228 }
229}
230
231fn delete_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
232 let id = match extract_user_id(req) {
233 Ok(id) => id,
234 Err(r) => return std::future::ready(r),
235 };
236 let removed = with_db(|db| db.users.remove(&id).is_some());
237 if removed {
238 std::future::ready(Response::with_status(StatusCode::NO_CONTENT))
239 } else {
240 std::future::ready(json_error(
241 StatusCode::NOT_FOUND,
242 &format!("User {id} not found"),
243 ))
244 }
245}
246
247#[allow(clippy::too_many_lines)]
252fn main() {
253 *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 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 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 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 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 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 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 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 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 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 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}