Start
Build a full-stack product catalog
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.
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: React, Angular, 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 rhc applies:
|
Note
|
rhc is available from RESTHeart 9.8.
|
npm install -g @restheart-cloud/cli
rhc login
rhc setup --srv <srvId> --dry-run # what the service is missing
rhc setup --srv <srvId> # 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:
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
}'
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
// 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
# 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:
# 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
# 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
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:
# 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}"
# 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}"
// 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.
| Starter | What it is |
|---|---|
Auth and multi-tenancy, on |
|
The same, on |
|
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 the Cloud Kit — sign-up, sign-in, teams and payments — and
configured by 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 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
git clone https://github.com/SoftInstigate/restheart-cloud-examples.git
cd restheart-cloud-examples
2. Choose Your Framework
# 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:
cp .env.example .env
# Edit .env and set VITE_RESTHEART_URL to your RESTHeart Cloud instance URL
For Angular:
# Edit src/environments/environment.ts and set restHeartUrl to your instance URL
4. Install Dependencies and Run
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:
Without these steps, the frontend applications won’t be able to fetch data from your RESTHeart Cloud instance. |
Learn more
-
The Cloud Kit - sign-up, sign-in, teams and payments for the frontend
-
Users and Permissions - Deep dive into ACL
-
RESTHeart Documentation - Full documentation
-
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" class="bare">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: