= Build a full-stack product catalog :nav-title: Full-stack example :description: A complete web app on RESTHeart Cloud: the collections, sample data and permissions of a product catalog, then a React, Vue or Angular frontend over it. :keywords: full-stack tutorial, React backend, Vue backend, Angular backend, product catalog, REST API tutorial, MongoDB web app :group: Start :order: 60 This page builds a product catalog end to end: the backend on your service, then a frontend in React, Vue or Angular that searches and filters it. Under ten minutes, and every request is spelled out so you know what the app does. TIP: **Starting something real?** Fork a starter instead of following along. Each one ships sign-up, login, OAuth, invitations and teams already working, and an `rhc.setup.ts` that configures your service in one command: https://github.com/SoftInstigate/restheart-cloud-starter-react[React^], https://github.com/SoftInstigate/restheart-cloud-starter-ng[Angular^], https://github.com/SoftInstigate/restheart-cloud-starter-ecommerce[Ecommerce^] with catalog, cart and Stripe checkout. This page is still where the calls a starter makes are explained. == What you will build * Products, categories and inventory as collections, with sample data. * Search by name, filter by price and category, from the API. * A permission that lets the public read the catalog and nobody else write it. * A single-page frontend that uses it, in the framework you prefer. == Part 1: Backend Setup === The short way The whole of Part 1 — collections, indexes, sample data and permissions — is what a setup file states, and xref:cli.adoc[`rhc`] applies: NOTE: `rhc` is available from RESTHeart 9.8. [source,bash] ---- npm install -g @restheart-cloud/cli rhc login rhc setup --srv --dry-run # what the service is missing rhc setup --srv # make it so ---- Every step is a check and an apply, so running it again writes nothing. Read on for what each of those steps does through the API — worth knowing whether or not you let `rhc` do it. === Step 1: Create Collections First, create the collections for products, categories, and inventory: [.tabs] ==== cURL:: + -- [source,bash] ---- export ROOT_PASSWORD='the password you set on Connect' # Create collections curl -i -X PUT https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -u root:$ROOT_PASSWORD curl -i -X PUT https://f3a9c1.eu-central-1-free-1.restheart.com/categories \ -u root:$ROOT_PASSWORD curl -i -X PUT https://f3a9c1.eu-central-1-free-1.restheart.com/inventory \ -u root:$ROOT_PASSWORD # Create permission for unauthenticated read access to products curl -i -X POST https://f3a9c1.eu-central-1-free-1.restheart.com/acl \ -u root:$ROOT_PASSWORD -H "Content-Type: application/json" \ -d '{ "_id": "allCanGetProducts", "roles": ["$unauthenticated"], "predicate": "path(/products) and method(GET)", "priority": 100 }' ---- -- HTTPie:: + -- [source,bash] ---- export ROOT_PASSWORD='the password you set on Connect' # Create collections http PUT https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -a root:$ROOT_PASSWORD http PUT https://f3a9c1.eu-central-1-free-1.restheart.com/categories \ -a root:$ROOT_PASSWORD http PUT https://f3a9c1.eu-central-1-free-1.restheart.com/inventory \ -a root:$ROOT_PASSWORD # Create permission for unauthenticated read access to products http POST https://f3a9c1.eu-central-1-free-1.restheart.com/acl \ -a root:$ROOT_PASSWORD \ Content-Type:application/json \ _id="allCanGetProducts" \ roles:='["$unauthenticated"]' \ predicate="path(/products) and method(GET)" \ priority:=100 ---- -- JavaScript:: + -- [source,javascript] ---- // Create collections const createCollections = async () => { const headers = { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD) }; fetch('https://f3a9c1.eu-central-1-free-1.restheart.com/products', { method: 'PUT', headers }) .then(response => { if (response.ok) { console.log('Products collection created successfully'); } else { console.error('Failed to create products collection:', response.status); } }) .catch(error => console.error('Error:', error)); fetch('https://f3a9c1.eu-central-1-free-1.restheart.com/categories', { method: 'PUT', headers }) .then(response => { if (response.ok) { console.log('Categories collection created successfully'); } else { console.error('Failed to create categories collection:', response.status); } }) .catch(error => console.error('Error:', error)); fetch('https://f3a9c1.eu-central-1-free-1.restheart.com/inventory', { method: 'PUT', headers }) .then(response => { if (response.ok) { console.log('Inventory collection created successfully'); } else { console.error('Failed to create inventory collection:', response.status); } }) .catch(error => console.error('Error:', error)); }; // Create permission for unauthenticated read access to products const createProductsReadPermission = () => { fetch('https://f3a9c1.eu-central-1-free-1.restheart.com/acl', { method: 'POST', headers: { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD), 'Content-Type': 'application/json' }, body: JSON.stringify({ _id: "allCanGetProducts", roles: ["$unauthenticated"], predicate: "path(/products) and method(GET)", priority: 100 }) }) .then(response => { if (response.ok) { console.log('Products read permission created successfully'); } else { console.error('Failed to create products read permission:', response.status); } }) .catch(error => console.error('Error:', error)); }; // Execute createCollections(); createProductsReadPermission(); ---- -- ==== === Step 2: Add Sample Data Instead of manually creating each product, you can download sample data files and import them to your instance. First, download the sample data files: ==== Download Sample Data [source,bash] ---- # Download categories curl -o categories.json https://restheart.org/assets/categories.json # Download products curl -o products.json https://restheart.org/assets/products.json ---- Now import the data to your RESTHeart instance: [.tabs] ==== cURL:: + -- [source,bash] ---- # Import categories curl -X POST https://f3a9c1.eu-central-1-free-1.restheart.com/categories \ -u root:$ROOT_PASSWORD \ -H "Content-Type: application/json" \ -d @categories.json # Import products curl -X POST https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -u root:$ROOT_PASSWORD \ -H "Content-Type: application/json" \ -d @products.json ---- -- HTTPie:: + -- [source,bash] ---- # Import categories http POST https://f3a9c1.eu-central-1-free-1.restheart.com/categories \ -a root:$ROOT_PASSWORD \ Content-Type:application/json < categories.json # Import products http POST https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -a root:$ROOT_PASSWORD \ Content-Type:application/json < products.json ---- -- JavaScript:: + -- [source,javascript] ---- const fs = require('fs'); // Read and import categories const importCategories = async () => { try { const categories = JSON.parse(fs.readFileSync('categories.json', 'utf8')); const response = await fetch('https://f3a9c1.eu-central-1-free-1.restheart.com/categories', { method: 'POST', headers: { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD), 'Content-Type': 'application/json' }, body: JSON.stringify(categories) }); if (response.ok) { console.log('Categories imported successfully'); } else { console.error('Failed to import categories:', response.status); } } catch (error) { console.error('Error importing categories:', error); } }; // Read and import products const importProducts = async () => { try { const products = JSON.parse(fs.readFileSync('products.json', 'utf8')); const response = await fetch('https://f3a9c1.eu-central-1-free-1.restheart.com/products', { method: 'POST', headers: { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD), 'Content-Type': 'application/json' }, body: JSON.stringify(products) }); if (response.ok) { console.log('Products imported successfully'); } else { console.error('Failed to import products:', response.status); } } catch (error) { console.error('Error importing products:', error); } }; // Execute imports (async () => { await importCategories(); await importProducts(); console.log('Data import completed!'); })(); ---- -- ==== === Step 3: Test Your API Test the API endpoints to make sure everything is working: [.tabs] ==== cURL:: + -- [source,bash] ---- # Search products by name curl -i "https://f3a9c1.eu-central-1-free-1.restheart.com/products" \ -u root:$ROOT_PASSWORD \ -G --data-urlencode "filter={'name':{'$regex':'headphones','$options':'i'}}" # Filter by price range curl "https://f3a9c1.eu-central-1-free-1.restheart.com/products" \ -u root:$ROOT_PASSWORD \ -G --data-urlencode "filter={'price':{'$gte':50,'$lte':150}}" # Get products with low inventory curl "https://f3a9c1.eu-central-1-free-1.restheart.com/products" \ -u root:$ROOT_PASSWORD \ -G --data-urlencode "filter={'quantity':{'$lt':10}}" # Category-based filtering with sorting curl "https://f3a9c1.eu-central-1-free-1.restheart.com/products" \ -u root:$ROOT_PASSWORD \ -G --data-urlencode "filter={'category':'electronics'}" \ -G --data-urlencode "sort={'price':1}" ---- -- HTTPie:: + -- [source,bash] ---- # Search products by name http GET https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -a root:$ROOT_PASSWORD \ filter=="{'name':{\$regex:'headphones',\$options:'i'}}" # Filter by price range http GET https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -a root:$ROOT_PASSWORD \ filter=="{'price':{\$gte:50,\$lte:150}}" # Get products with low inventory http GET https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -a root:$ROOT_PASSWORD \ filter=="{'quantity':{\$lt:10}}" # Category-based filtering with sorting http GET https://f3a9c1.eu-central-1-free-1.restheart.com/products \ -a root:$ROOT_PASSWORD \ filter=="{'category':'electronics'}" sort=="{price:1}" ---- -- JavaScript:: + -- [source,javascript] ---- // Search products by name const searchByName = () => { const filter = encodeURIComponent("{'name':{\$regex:'headphones',\$options:'i'}}"); fetch(`https://f3a9c1.eu-central-1-free-1.restheart.com/products?filter=${filter}`, { headers: { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD) } }) .then(response => response.json()) .then(data => { console.log('Search results:', data); }) .catch(error => console.error('Error:', error)); }; // Filter by price range const filterByPriceRange = () => { const filter = encodeURIComponent("{'price':{\$gte:50,\$lte:150}}"); fetch(`https://f3a9c1.eu-central-1-free-1.restheart.com/products?filter=${filter}`, { headers: { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD) } }) .then(response => response.json()) .then(data => { console.log('Price range results:', data); }) .catch(error => console.error('Error:', error)); }; // Get products with low inventory const getLowInventory = () => { const filter = encodeURIComponent("{'quantity':{\$lt:10}}"); fetch(`https://f3a9c1.eu-central-1-free-1.restheart.com/products?filter=${filter}`, { headers: { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD) } }) .then(response => response.json()) .then(data => { console.log('Low inventory products:', data); }) .catch(error => console.error('Error:', error)); }; // Category-based filtering with sorting const filterByCategory = () => { const filter = encodeURIComponent("{'category':'electronics'}"); const sort = encodeURIComponent("{price:1}"); fetch(`https://f3a9c1.eu-central-1-free-1.restheart.com/products?filter=${filter}&sort=${sort}`, { headers: { 'Authorization': 'Basic ' + btoa('root:' + process.env.ROOT_PASSWORD) } }) .then(response => response.json()) .then(data => { console.log('Category results:', data); }) .catch(error => console.error('Error:', error)); }; // Execute searchByName(); filterByPriceRange(); getLowInventory(); filterByCategory(); ---- -- ==== == Part 2: Frontend Setup Now that your backend is ready, let's build the frontend! We've created three complete implementations using modern JavaScript frameworks. === Choose Your Framework **For an application you intend to keep**, fork a starter. They are scaffolds rather than demos: signup, login, OAuth, email verification, password reset, team invitations and a team switcher all work out of the box, and each ships an `rhc.setup.ts` that configures the service to match. [cols="1,3"] |=== | Starter | What it is | https://github.com/SoftInstigate/restheart-cloud-starter-react[React^] | Auth and multi-tenancy, on `@restheart-cloud/kit-react` | https://github.com/SoftInstigate/restheart-cloud-starter-ng[Angular^] | The same, on `@restheart-cloud/kit-ng`, with SSR for public routes | https://github.com/SoftInstigate/restheart-cloud-starter-ecommerce[Ecommerce (React)^] | The above plus a shop: catalog, cart, Stripe Checkout, order ledger, guest checkout |=== Their feature flags are read by the setup file rather than repeated in it, so the app and the service cannot drift apart. All three are built on xref:kit.adoc[the Cloud Kit] — sign-up, sign-in, teams and payments — and configured by xref:cli.adoc[`rhc`] from an `rhc.setup.ts` committed alongside the code. **To follow this page's catalog example specifically**, the three small single-purpose demos are at https://github.com/SoftInstigate/restheart-cloud-examples[restheart-cloud-examples^]. Each implementation is a complete single-page application featuring: * **Product search by name** - Real-time search functionality * **Price range filtering** - Interactive price filters * **Category filtering** - Browse products by category * **Real-time updates** - Live data from RESTHeart Cloud * **Responsive design** - Works on desktop, tablet, and mobile * **Production-ready code** - Best practices for each framework === Getting Started with the Frontend ==== 1. Clone the Repository [source,bash] ---- git clone https://github.com/SoftInstigate/restheart-cloud-examples.git cd restheart-cloud-examples ---- ==== 2. Choose Your Framework [source,bash] ---- # For Vue.js cd vue-product-search # OR for React cd react-product-search # OR for Angular cd angular-product-search ---- ==== 3. Configure Your RESTHeart Cloud Instance For Vue.js and React: [source,bash] ---- cp .env.example .env # Edit .env and set VITE_RESTHEART_URL to your RESTHeart Cloud instance URL ---- For Angular: [source,bash] ---- # Edit src/environments/environment.ts and set restHeartUrl to your instance URL ---- ==== 4. Install Dependencies and Run [source,bash] ---- npm install npm run dev # For Vue.js and React npm start # For Angular ---- [NOTE] ==== Make sure you've completed the backend setup steps above, including: * Creating the `products` collection * Adding sample product data * Configuring the ACL for unauthenticated read access Without these steps, the frontend applications won't be able to fetch data from your RESTHeart Cloud instance. ==== === Learn more * xref:kit.adoc[The Cloud Kit] - sign-up, sign-in, teams and payments for the frontend * xref:users-and-permissions.adoc[Users and Permissions] - Deep dive into ACL * https://restheart.org/docs[RESTHeart Documentation^] - Full documentation * https://github.com/SoftInstigate/restheart-cloud-examples[GitHub Examples^] - Source code and more examples == Troubleshooting === Frontend Can't Connect to Backend * Verify your RESTHeart URL is correct in the environment configuration * Check that the ACL permission for unauthenticated access is created * Ensure your RESTHeart Cloud instance is running * Check browser console for CORS errors === No Products Showing * Verify you've added sample data using the backend setup steps * Check the products collection exists: `curl https://f3a9c1.eu-central-1-free-1.restheart.com/products` * Verify the ACL allows unauthenticated GET requests to /products === Search/Filter Not Working * Check that your MongoDB queries are properly formatted * Verify the filter parameters are correctly URL-encoded * Test the API endpoints directly with cURL or HTTPie * Check for JavaScript errors in the browser console == Support Need help? Here are some resources: * https://github.com/SoftInstigate/restheart-cloud-examples/issues[Report Issues on GitHub^] * https://github.com/SoftInstigate/restheart/discussions[Community Discussions^] * mailto:support@restheart.com[Email Support]