You'll build a cart whose contents live in the session on the server. Adding an item is one typed call from the browser. The server holds the only copy of the cart, so every page reads the same quantities from the session.
Before you start
Install from crates.io. You need no Node and no package manager.
An action is a function that runs on the server and is callable by id from the browser. Put these in app/routes/cart/actions.ts and they belong to the /cart route.
session is the visitor's session, already loaded. Assigning to it persists it. input is typed as AddToCart from the schema. A request whose body doesn't match is refused before your code runs.
import{actions,typeCartProps}from"@generated/client";exportdefaultfunctionCart({lines}:CartProps){constitems=lines.reduce((n,l)=>n+Number(l.quantity),0);return(<section><p>{items} in your cart</p><ul>{lines.map((line)=>(<likey={String(line.id)}>{line.name} × {String(line.quantity)}<buttononClick={()=>voidactions.cart.addToCart({product_id:line.id,quantity:1n})}>+</button><buttononClick={()=>voidactions.cart.addToCart({product_id:line.id,quantity:-1n})}>-</button><buttononClick={()=>voidactions.cart.removeFromCart({product_id:line.id})}>Remove</button></li>))}</ul></section>);}
actions.cart.addToCart is generated. If you pass it the wrong shape, the build reports it before anything reaches the browser. A successful call revalidates the route, so the loader runs again and the list shows the new quantities.
LiveThe cart page from this tutorial, with an Add button for each product
0 in your cart
This cart is in your session on this site's server. Add something, reload the page and it's still there.
A guard runs before anything else the action would do. If it refuses, no service is called and nothing is written. Add a checkout action to the same file, below the other two:
app/routes/cart/actions.ts
exportconstcheckout=action(async({session,services}:ActionCtx)=>{constlines=Object.entries(session.cart).map(([id,quantity])=>({product_id:BigInt(id),quantity}));if(lines.length===0)fail("invalid","the cart is empty");constorder=awaitservices.shopping.placeOrder({lines});session.cart={};returnorder;});
fail("invalid", ...) becomes an ActionFailure in the browser with a status the host chose from the kind. With an empty cart, the order service is never called.