= Build a full-stack product catalog :nav-title: Full-stack example :description: Build the backend of a product catalog, request by request: collections, sample data, a public read permission and queries. Then add a frontend from a starter. :keywords: full-stack tutorial, React backend, Vue backend, Angular backend, product catalog, REST API tutorial, MongoDB web app :group: Start :order: 60 This page is a tutorial. You build the backend of a product catalog on your service, one request at a time, then add a frontend from a starter. It takes under ten minutes, and every request is written out so you see what the app does. TIP: **For a real app**, fork a starter. Each one has sign-up, login, OAuth, invitations and teams already working, and a `ulabase.setup.ts` that configures your service in one command: https://github.com/ulabase/starter-react[React^], https://github.com/ulabase/starter-ng[Angular^], https://github.com/ulabase/starter-vue[Vue^], https://github.com/ulabase/starter-ecommerce[Ecommerce^] with catalog, cart and Stripe checkout. This page explains the calls a starter makes. == 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 anyone read the products without signing in. * A frontend from a starter, in React, Angular or Vue. == Part 1: the backend === The short way: a starter and its setup file The steps below build the catalog request by request. To have a working shop at once instead, take the ecommerce starter: its `ulabase.setup.ts`, in the root of the repository, configures your service in one command, with the catalog, a public read permission on it, the orders, and sign-up and login. [source,bash] ---- git clone https://github.com/ulabase/starter-ecommerce.git cd starter-ecommerce npm install # also installs the setup file's dependencies npm install -g ulabase # the ulabase command ulabase login # asks for a personal access token: your profile, Tokens ulabase setup --srv ---- `ulabase setup` reads `ulabase.setup.ts` from the folder you run it in. Each step checks first and writes only what is missing, so running it again changes nothing. Its last step writes the service URL into `src/environments/environment.ts`, so `npm run dev` opens a shop on your service. The shop then works with its catalog, cart and accounts; checkout turns on when you add the Stripe test keys and run the setup again, as the starter's README explains. See xref:cli.adoc[`ulabase`] for the command, and xref:tokens.adoc[Tokens] for the personal access token. === Step 1: create the collections Create the collections for products, categories and inventory: [.tabs] ==== cURL:: + -- [source,bash] ---- export ROOT_PASSWORD='your root password' # Create collections curl -i -X PUT https://c0ffee.ulabase.app/products \ -u root:$ROOT_PASSWORD curl -i -X PUT https://c0ffee.ulabase.app/categories \ -u root:$ROOT_PASSWORD curl -i -X PUT https://c0ffee.ulabase.app/inventory \ -u root:$ROOT_PASSWORD # Create permission for unauthenticated read access to products curl -i -X POST https://c0ffee.ulabase.app/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='your root password' # Create collections http PUT https://c0ffee.ulabase.app/products \ -a root:$ROOT_PASSWORD http PUT https://c0ffee.ulabase.app/categories \ -a root:$ROOT_PASSWORD http PUT https://c0ffee.ulabase.app/inventory \ -a root:$ROOT_PASSWORD # Create permission for unauthenticated read access to products http POST https://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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 Download the sample data files and import them into your service. ==== Download the sample data [source,bash] ---- # Download categories curl -o categories.json https://ulabase.com/assets/examples/categories.json # Download products curl -o products.json https://ulabase.com/assets/examples/products.json ---- Import the data into your service: [.tabs] ==== cURL:: + -- [source,bash] ---- # Import categories curl -X POST https://c0ffee.ulabase.app/categories \ -u root:$ROOT_PASSWORD \ -H "Content-Type: application/json" \ -d @categories.json # Import products curl -X POST https://c0ffee.ulabase.app/products \ -u root:$ROOT_PASSWORD \ -H "Content-Type: application/json" \ -d @products.json ---- -- HTTPie:: + -- [source,bash] ---- # Import categories http POST https://c0ffee.ulabase.app/categories \ -a root:$ROOT_PASSWORD \ Content-Type:application/json < categories.json # Import products http POST https://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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 Try a few queries to check that everything works: [.tabs] ==== cURL:: + -- [source,bash] ---- # Search products by name curl -i "https://c0ffee.ulabase.app/products" \ -u root:$ROOT_PASSWORD \ -G --data-urlencode 'filter={"name":{"$regex":"headphones","$options":"i"}}' # Filter by price range curl "https://c0ffee.ulabase.app/products" \ -u root:$ROOT_PASSWORD \ -G --data-urlencode 'filter={"price":{"$gte":50,"$lte":150}}' # Get products with low inventory curl "https://c0ffee.ulabase.app/products" \ -u root:$ROOT_PASSWORD \ -G --data-urlencode 'filter={"quantity":{"$lt":10}}' # Category-based filtering with sorting curl "https://c0ffee.ulabase.app/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://c0ffee.ulabase.app/products \ -a root:$ROOT_PASSWORD \ filter=="{'name':{\$regex:'headphones',\$options:'i'}}" # Filter by price range http GET https://c0ffee.ulabase.app/products \ -a root:$ROOT_PASSWORD \ filter=="{'price':{\$gte:50,\$lte:150}}" # Get products with low inventory http GET https://c0ffee.ulabase.app/products \ -a root:$ROOT_PASSWORD \ filter=="{'quantity':{\$lt:10}}" # Category-based filtering with sorting http GET https://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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://c0ffee.ulabase.app/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: the frontend With the backend ready, build the frontend. === Choose a starter **For an application you intend to keep**, fork a starter. Each is a base for a real app: signup, login, OAuth, email verification, password reset, team invitations and a team switcher already work, and each has a `ulabase.setup.ts` that configures the service to match. [cols="1,3"] |=== | Starter | What it is | https://github.com/ulabase/starter-react[React^] | Auth and multi-tenancy, on `@ulabase/kit-react` | https://github.com/ulabase/starter-ng[Angular^] | The same, on `@ulabase/kit-ng`, with SSR for public routes | https://github.com/ulabase/starter-vue[Vue^] | The same, on `@ulabase/kit-vue` | https://github.com/ulabase/starter-ecommerce[Ecommerce (React)^] | The above plus a shop: catalog, cart, Stripe Checkout, order ledger, guest checkout |=== The setup file reads the app's feature flags, so the app and the service stay in step. All four use xref:kit.adoc[the Ulabase Kit] for sign-up, sign-in, teams and payments, and xref:cli.adoc[`ulabase`] configures their service from a `ulabase.setup.ts` committed with the code. === Run the ecommerce starter The ecommerce starter is this page's catalog grown into a shop: [source,bash] ---- git clone https://github.com/ulabase/starter-ecommerce.git cd starter-ecommerce npm install ---- Then `ulabase setup --srv `, which also writes the service URL into the app, and `npm run dev`. Its README has the details, Stripe included. === Learn more * xref:kit.adoc[The Ulabase Kit]: sign-up, sign-in, teams and payments for the frontend * xref:users-and-permissions.adoc[Users and permissions through the API]: permissions in detail * https://github.com/ulabase[The starters on GitHub^]: source code of the React, Angular, Vue and ecommerce starters == Troubleshooting === The frontend cannot reach the service * Check `apiUrl` in the app's environment file: `ulabase setup --srv ` writes it * Check that the permission for unauthenticated reads exists * Check in the console that the service is active * Look for CORS errors in the browser console === No products show * Check that you imported the sample data (Step 2) * Check that the products collection exists: `curl https://c0ffee.ulabase.app/products` * Check that the permission allows unauthenticated `GET` requests to `/products` === Search or filter does not work * Check the syntax of your MongoDB queries * Check that the filter parameters are URL-encoded * Run the same query with cURL or HTTPie * Look for JavaScript errors in the browser console == Support For help, open a ticket from the **Support** page of your service. The answer arrives by email and stays under the ticket.