Recipe: JSON API
Build a small notes API with logging, CORS, JSON, and bearer auth — the same
shape as examples/api.
Dependencies
[dependencies]
churust = { version = "0.3", features = ["full"] }
serde = { version = "1", features = ["derive"] }
Code
use churust::prelude::*; use serde::{Deserialize, Serialize}; use std::sync::Mutex; #[derive(Clone, Serialize, Deserialize)] struct Note { id: u64, text: String, } #[derive(Deserialize)] struct NewNote { text: String, } #[derive(Clone, Debug)] struct AdminUser { _name: String, } struct Store { notes: Mutex<Vec<Note>>, } #[churust::main] async fn main() -> std::io::Result<()> { Churust::server() .host("127.0.0.1") .port(8080) .state(Store { notes: Mutex::new(Vec::new()), }) .install(CallLogging::new()) .install(ContentNegotiation::new()) .install(Cors::new().allow_origin("http://localhost:3000")) .install(Auth::bearer(|token: String| async move { // Demo only — use secure_compare + a real store in production. if token == "admin-token" { Some(AdminUser { _name: "admin".into(), }) } else { None } })) .routing(|r| { r.get("/notes", |s: State<Store>| async move { let notes = s.notes.lock().unwrap().clone(); Json(notes) }); r.post( "/notes", |Principal(_admin): Principal<AdminUser>, s: State<Store>, Json(input): Json<NewNote>| async move { let mut notes = s.notes.lock().unwrap(); let id = notes.len() as u64 + 1; let note = Note { id, text: input.text, }; notes.push(note.clone()); (StatusCode::CREATED, Json(note)) }, ); }) .start() .await }
Try it
cargo run -p api # from the Churust repo
curl localhost:8080/notes
# []
curl -X POST localhost:8080/notes -d '{"text":"hi"}'
# {"error":"authentication required","status":401}
curl -X POST localhost:8080/notes \
-H 'authorization: Bearer admin-token' \
-H 'content-type: application/json' \
-d '{"text":"hi"}'
# {"id":1,"text":"hi"}
Takeaways
Principal<AdminUser>makes the POST route uncallable without auth.ContentNegotiationkeeps error bodies JSON-friendly.- CORS names real browser origins — not
*— when credentials matter.