Learn how to create a robust shopping cart for the web using the Qwik framework and Turso, the edge database.
A shopping cart is an e-commerce software that enables visitors to purchase items on the web. It is one of the most prevalent programs that users of the web come across. If your company sells things online, it's likely that you've purchased, built, or are subscribed to such a service.
Shopping carts facilitate e-commerce and are of significant importance to merchants since they automate product listing, the verification of payment cards, the storage and management of customer details and their orders, and lastly, checking out. Customers get to manage their orders giving them more control on the amounts they'll be required to pay at the end.
In the modern accelerated world of short attention spans, users expect to be able to complete tasks fast, regardless of the quality of their internet or where they are located in the world.
We can use Qwik to to render the web experience as close to user's expectations because the framework makes it easy to provide users with an instant-on web experience regardless of the size of the app or type of device they are accessing it through. We employ Turso to handle the data persistence side of the puzzle, since it is a database at the edge, meaning it will be able to provide data from the closest possible location to the user, hence reducing latency which translates to a faster browsing experience. Turso does all this without neglecting the data security obligations it has towards its user data.
Research shows that slow websites:
As established, we'll employ Qwik on the front-end side of the equation and use Turso to persist the shopping data. We won't address user authentication on this post and instead assume that the user is already signed in.
In this post I'll break down the implementation of a shopping cart into several parts:
Pre-requisites:
You need the latest LTS version of node.js and the Turso CLI installed in your system to proceed.
We will not cover the creation of a Qwik project and Turso database as we have excellent resources to follow on that topic. There is a GitHub repository that has the full source code of the shopping cart being created here, you can use its README to assist you with setting up the database, its schema, and indexes.
Follow the data seeding instructions to add some demo data to the database if you'll opt to go straight to the repo and skip this post.
As in most apps, we'll first need a set of utility functions that we will be referencing in the steps that follow. We'll place these functions under the /src/utils/
directory
The first utility function is the Turso client that exports a libSQL client instance.
// /src/utils/turso-db.ts
import { createClient } from "@libsql/client";
const config = {
url: import.meta.env.VITE_TURSO_DATABASE_URL,
authToken: import.meta.env.VITE_TURSO_DATABASE_TOKEN
}
export const client = createClient(config);
And lastly the types that we are going to use within the app, which can also be found in this file.
For product listings we make database requests that query data by category and display it on the listing pages.
For example, in the category page where we list products based on shared categories whose id
s are passed as part of the page URL, we have the following Turso query.
client.execute(
sql: "select * from products where category_id = ?",
args: [categoryId]
);
We add the above query inside Qwik's routeLoader$
where we are able to access the route parameters and collect the category id categoryId
which we then use to filter the data being queried from the database as can be viewed in the code block above.
When you check this source code file on the repository you'll see that we have useCategoryLoader
as the routeLoader$
within this page. We then get the data returned by this loader inside the Qwik page component as a reactive Signal
as follows.
export default component$(() => {
const pageData = useCategoryLoader();
// other code
}
Afterwards, we can list the items on the template like this.
return (
<!-- -->
<ul class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{
pageData.value.products.length > 0
? pageData.value.products.map((product: Product) => (<ProductCard product={product} key={product.id} />))
: <div class="p-8">No items</div>
}
</ul>
<!-- -->
)
For the most important feature of the shopping cart, we are aiming for two things
We handle persisting the data to Turso by adding a function that inserts the data whenever a user clicks on the “Add to cart” button.
/* /src/components/product-card/ProductCard.tsx */
const addToCart = $(async () => {
try {
await client.execute({
sql: "insert into cart_items(product_id, user_id) values(?, ?)",
args: [props.product.id, authenticatedUser.value.id]
});
const cartItem = await client.execute({
sql: "select * from cart_items where product_id = ? and user_id = ?",
args: [props.product.id, authenticatedUser.value.id]
});
appState.cart.items.push(cartDataAdapter(cartItem.rows)[0])
} catch (error) {
// TODO: Catch error and notify user
}
})
return (
<button
class="block w-full rounded bg-yellow-400 p-4 text-sm font-medium transition hover:scale-105"
onClick$={addToCart}
>
Add to Cart
</button>
)
To enable users to preview the cart details wherever they are on the site, add a cart component (mini cart) under /src/components
making it occupy a fixed position on the page, and add ability to toggle its visibility.
Here is how we've done this using Qwik: checking this file you'll notice that we are toggling the app context's cart.show
value stored in the appState
and also setting it to false whenever the Esc
key is pressed.
const closeCart = $(() => {
appState.cart.show = !appState.cart.show;
});
// * hide cart when esc key is pressed
useOn("keydown", $((event: unknown) => {
if((event as KeyboardEvent).key === "Escape") {
appState.cart.show = false;
}
}))
To list the cart items within this mini cart, fetch the items stored under the currently logged in user from the cart_items
table as follows.
/* /src/components/cart/Cart.tsx */
useTask$(async () => {
try {
const storedCartItems = await client.execute({
sql: "select cart_items.count, cart_items.id as cart_item_id, products.* from cart_items left join products on products.id = cart_items.product_id where user_id = ?",
args: [authenticatedUser.value.id]
});
appState.cart.items = cartDataAdapter(storedCartItems.rows);
} catch (error) {
// TODO: Catch error and notify user
}
})
The cartDataAdapter function helps us convert the collected data into the app's CartItem
type.
We are using Qwik's useTask$
hook in the above code block since it's called at least once when the component is created. This is beneficial for our app as the request is made in the server layer when the page loads, hence reducing the number of requests made on the client side. If the app is not making a new page load the request will still be made on the client layer and we'll have the mini cart populated with data anyway.
For the cart page, the data fetch requests made are more or less the same as within the mini cart with the difference being that they are made inside a routeLoader$
.
One good practice inside the cart page is the placement of recommendations right at the bottom of the cart on display. This enables users to either see alternatives or add items that they'd like to have together with the items within the cart. Use the data collected over time within your shopping cart to come up with an algorithm that better suits the items to be placed here.
In our shopping cart we have a Recommendations component placed inside the cart page that displays four items that exclude any of the ones existing in the cart, picked randomly from the products table.
/* /src/components/recommendations/Recommendations.tsx */
const placeholders = appState.cart.items.map(() => {
return "?"
}).join(",")
const values = appState.cart.items.map(item => {
return item.product.id;
})
try {
const response = await client.execute({
sql: `select * from products where id not in (${placeholders}) order by random() limit 4`,
args: values
});
recommendedProducts.value = response.rows as unknown as Product[];
} catch (error) {
// TODO: Catch error and notify user
}
17% of consumers abandon their carts due to long or complicated checkout processes. To avoid making your users part of this statistic, go for the simplified one-page checkout process.
In the checkout code of the shopping cart, you can observe that we've opted for the simplified one page checkout where the user is presented with all fill in forms from his contact information, shipping details, to payment cards.
Within the checkout, just like in the cart we are fetching the cart data persisted on Turso and listing it as the order summary of the items that the user is buying.
After funnelling users through, you eventually want them to place orders and make your efforts in creating this whole thing worthwhile.
In this section I'm not going to give or suggest a payment option, choose your own payment service and plug it in. I will only explain and try to demonstrate what happens when a user clicks the “Pay Now” button we saw in the previous step.
After verifying and making sure the payment went through, submit the items currently in the user's cart_items
table as a new order, attaching this order with the customer contact information and address just as required by the orders
table.
await client.execute({
sql: "insert into orders(user_id, customer_name, amount, shipping_fees, discount_amt, final_amount, shipping_address, paid) values(?, ?, ?, ?, ?, ?, ?, ?)",
args: [authenticatedUser.id, `${authenticatedUser.first_name} ${authenticatedUser.last_name}`, amount, calculatedShippingFees, discountAmount, finalAmount, `${shippingAddress.zipCode} ${shippingAddress.country}`, true]
});
Then clear the items from the cart_items
table after having transferred them to the order_items
table.
/* /src/routes/checkout/index.tsx */
const transaction = await client.transaction();
const newOrder = await transaction.execute({
sql: "insert into orders(user_id, customer_name, amount, shipping_fees, discount_amt, final_amount, shipping_address, paid) values(?, ?, ?, ?, ?, ?, ?, ?)",
args: [authenticatedUser.id, `${authenticatedUser.first_name} ${authenticatedUser.last_name}`, amount, calculatedShippingFees, discountAmount, finalAmount, `${shippingAddress.zipCode} ${shippingAddress.country}`, true]
});
for(const item of appState.cart.items){
transaction.execute({
sql: "insert into order_items(order_id, product_id, count) values(?, ?, ?);",
args: [newOrder.lastInsertRowid as unknown as number, item.product.id, item.count]
});
transaction.execute({
sql: "delete from cart_items where id = ?;",
args: [item.id]
});
}
await transaction.commit();
Inside the project this is done in Qwik's server$
function as seen here.
And that's how you create a shopping cart for your business using this Quick stack.
More features can be added to broaden the user experience but it ought to be done with functional relevance. Customer satisfaction is mainly what we are looking for when trying to get them to end up making purchases in e-commerce websites.
An example of a feature that could be added include user reviews and ratings which tend to foster more confidence in users since they can read about the experiences of others who've used the website before.
For more information on the stack used here, you can visit the following resources.