Backend unique lists
We changed our database - so it's able to handle different lists with different shopping items. But currently we have only one list - as its ID is hardcoded.
Let's change that in this chapter. We expand our REST
api in such a way, so we can create new lists and load existing ones, without needed to hardcode an ID (in the backend).
Changing the routes
We currently had the following route: /items
. But now that we can also have different lists, we should rename our exising routes.
Go to our main.rs
in the backend - and change the routes from:
/items
to/list/:list_uuid/items
/items/:uuid
to/list/:list_uuid/items/:item_uuid
Each item is now parametrized by a list, or a subresource of the list.
#![allow(unused)] fn main() { let app = Router::new() .route("/list/:list_uuid/items", get(get_items).post(add_item)) .route("/list/:list_uuid/items/:item_uuid", delete(delete_item)) .layer(CorsLayer::permissive()) .with_state(db); }
The :list_uuid
will be extracted from the path
and is from now on available in our controllers in the controllers.rs
module.
Perfect! Now let's use the new variable inside our controllers and get rid of the hardcoded LIST_UUID
in the controllers.rs
module.
Using new Path parameters
Now with the new routes, we can extract the extra list_uuid
parameter - and use it in our controllers.
If you have two path parameters, like in delete_item
, you now have to extract path parameters as a tuple.
The names inside the tuple bind to the names given in your route
path.
Let's start with the delete_item
, as it's the hardest (path wise):
#![allow(unused)] fn main() { pub async fn delete_item( State(state): State<Database>, Path((list_uuid, item_uuid)): Path<(Uuid, Uuid)>, ) -> impl IntoResponse { let Ok(mut db) = state.write() else { return StatusCode::SERVICE_UNAVAILABLE; }; db.delete_item(&list_uuid.to_string(), &item_uuid.to_string()); StatusCode::OK } }
Like stated above - your Path
extractor now needs to work with a tuple of type (Uuid,Uuid)
. So if one of those parameters is not parsable as an uuid, we will get a 400 error automatically (invalid input).
Let's change the other existing controllers respectively:
#![allow(unused)] fn main() { pub async fn add_item( Path(list_uuid): Path<Uuid>, State(state): State<Database>, Json(post_request): Json<PostShopItem>, ) -> impl IntoResponse { let item = ShoppingItem { title: post_request.title.clone(), creator: post_request.posted_by.clone(), }; let item_uuid = Uuid::new_v4().to_string(); let Ok(mut db) = state.write() else { return (StatusCode::SERVICE_UNAVAILABLE).into_response(); }; db.insert_item(&list_uuid.to_string(), &item_uuid, item); ( StatusCode::OK, Json(ShoppingListItem { title: post_request.title, posted_by: post_request.posted_by, uuid: item_uuid, }), ) .into_response() } }
and
#![allow(unused)] fn main() { pub async fn get_items( Path(list_uuid): Path<Uuid>, State(state): State<Database>, ) -> impl IntoResponse { let items: Vec<ShoppingListItem> = state.read().unwrap().as_vec(&list_uuid.to_string()); Json(items) } }
Great! Now we adapted our existing controllers and already use the new list_uuid
. You can now also safely delete the hardcoded LIST_UUID
value ;).
Adding new routes
Now that we adapted the existing routes, let's add a new one to create a fresh list.
As we only have to request a /list
endpoint without to provide any data - we can get away with a GET
endpoint.
Usually, when you want to create new resources server side, you would supply a post
with some data. But for our case we do not need this.
What we need though is a response for this endpoint, as we want to know the list_uuid
of the newly created list.
Go to the model create and add a new serializable
and deserializable
model to the lib.rs
:
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] pub struct CreateListResponse { pub uuid: String, } }
This model will also be deserialized in the frontend later. So also put a Deserialize from serde
on top of the struct.
Go ahead an create a new async function in the controller.rs
module in our backend
crate.
#![allow(unused)] fn main() { use model::{CreateListResponse, PostShopItem, ShoppingListItem}; // import what is needed. pub async fn create_shopping_list(State(state): State<Database>) -> impl IntoResponse { let uuid = Uuid::new_v4().to_string(); state.write().unwrap().create_list(&uuid); Json(CreateListResponse { uuid }) } }
The last thing remaining is to put the create_shopping_list
into our Router
. You know the drill!
Add it to the router with a /list
route - fetched via a GET
.
#![allow(unused)] fn main() { .route("/list", get(create_shopping_list)) }
We are getting somewhere! :). Let's hop on to change the frontend
also, to get/post and delete with the new routes, we just created.