Rev
notion image

Short Explanation

A while ago, I found an interesting library called rspc. rspc is a tRPC-like library but for rust, offering full type safety from backend to frontend. As I've been keeping an eye on Rust, it can be my entryway to deep dive into the Rust language with this library. Then I thought about refactoring my previous project SiBaBe and made it into a full-stack application.

Project Goals

An online shop for ordering products from Bima Bakery, utilizing Next.js for the front end and Rust for the back end.
Here’s what users can do:
  • Register and log in to their account
  • Browse and view products
  • Add products to their shopping cart
  • Search for products using keywords
  • View their shopping cart and adjust the quantity of items
  • Place an order
  • View order history
  • View the details of past orders
  • Leave a review on purchased products.
 
Here’s what admins can do:
  • CRUD products
  • View and accept/reject order
  • View annual report
  • Add production data

Tech Stack Used

  • Frontend side: React with Next.js, Zustand for state management, Typescript for strong typing, Mantine for modal component, Tremor for chart component, React Toastify for toast components, and Tailwind CSS for styling, Supabase Storage for S3 storage, Clerk for easy auth management, React Query for API calls
  • Backend side: rspc for the server (which used Axum under the hood to expose the API over HTTP), Prisma for ORM, Planetscale for database,

Spotlight

Fully typesafe from backend to frontend

This is the VERY interesting part of why I jumped into this project. You can define the API route in Rust and can call it from frontend with no extra magic.
notion image

Conventional Commit

In this project, I decided to use a conventional commit so the commit message is cleaner and easier to read. I've been using it since I think it is awesome.
I also configured it with Husky so the commit message is checked before committing.
notion image

Beautiful chart with Tremor

Found this beautiful component library that can be used to display data in charts easily
notion image

Sync Clerk user to Database

Since I used Clerk to manage authentication for users, data inevitably needs to make its way to the database. This synchronization is vital for identifying which data corresponds to each user. To achieve this, I implemented webhooks that monitor three key events: 'user.created,' 'user.update,' and 'user.deleted.' For instance, when an individual signs up on our application, Clerk triggers a webhook carrying the 'user.created' event. This allows us to capture the data and seamlessly insert it into our database
async fn users_handler( db: Extension<Arc<PrismaClient>>, headers: HeaderMap, body: Bytes ) -> Result<StatusCode, StatusCode> { #[derive(Serialize, Deserialize)] struct Payload { data: Data, object: String, r#type: String, } #[derive(Serialize, Deserialize)] #[serde(untagged)] enum Data { Deleted { deleted: bool, id: String, }, CreatedOrUpdated { created_at: u64, email_addresses: Vec<EmailAddress>, first_name: String, id: String, image_url: String, last_name: String, profile_image_url: String, public_metadata: serde_json::Value, updated_at: u64, username: String, }, } #[derive(Serialize, Deserialize)] struct EmailAddress { email_address: String, id: String, } let wh = Webhook::new( env::var("WEBHOOK_SECRET").expect("WEBHOOK_SECRET not found").as_str() ).map_err(|_| { println!("Webhook secret not found"); StatusCode::INTERNAL_SERVER_ERROR })?; let payload: Payload = serde_json::from_slice(&body).unwrap(); wh.verify(&body, &headers).map_err(|_| { println!("Webhook verification failed"); StatusCode::UNAUTHORIZED })?; match payload.data { Data::CreatedOrUpdated { id, username, first_name, last_name, email_addresses, public_metadata, .. } => { if payload.r#type == "user.created" { let name = format!("{} {}", first_name, last_name); let email = &email_addresses[0].email_address; let user = db .customers() .create(id, username, name, email.to_string(), vec![]) .exec().await; match user { Ok(_) => println!("User created"), Err(_) => println!("User not created"), } } else if payload.r#type == "user.updated" { if public_metadata["role"] == "admin" { let name = format!("{} {}", first_name, last_name); let email = &email_addresses[0].email_address; let create_admin = db._transaction().run(|db| async move { db.customers().delete(prisma::customers::id::equals(id.clone())).exec().await?; db.admins() .create(id, username, name, email.to_string(), vec![]) .exec().await .map(|_| ()) }).await; match create_admin { Ok(_) => println!("User promoted to admin"), Err(_) => println!("User failed to be promoted to admin"), } } } else { println!("Unknown user event type"); } Ok(StatusCode::OK) } Data::Deleted { id, .. } => { let user = db.customers().delete(prisma::customers::id::equals(id)).exec().await; match user { Ok(_) => println!("User deleted"), Err(_) => println!("User not deleted"), } Ok(StatusCode::OK) } } } pub(crate) fn webhooks(client: Arc<PrismaClient>) -> Router { Router::new().route("/webhooks", post(users_handler)).layer(Extension(client)) }

The Problems and How I Solved It

Navigating through Rust's challenges was initially tough due to its reputation for being a challenging language to pick up. As a newcomer to Rust, I encountered difficulties, which were further compounded when working with the early-stage development for rspc and Prisma client Rust due to limited documentation and resources. Nevertheless, I persevered and methodically resolved these obstacles to achieve positive results. One of my strategies involved reaching out to the maintainers on their Discord channel for assistance. I'm truly grateful to them for their fast and helpful responses to all of my questions.

Lesson Learned

Creating a project using Rust has been a valuable learning experience, especially as TypeScript is my daily go-to language. One of the standout features I discovered was the exceptional error handling capabilities inherent to the language itself. Unlike TypeScript, Rust's approach to error handling provided a more structured and disciplined way of dealing with issues. This difference emphasized the importance of building a solid foundation for error management, regardless of the programming language being used. Rust's error handling mechanisms have undoubtedly enriched my coding practices in both Rust and TypeScript projects.