A backend API system designed to support a Jewellery Product Detail Page (PDP). The system handles product information, metal & diamond customization, price calculation, and inventory-based availability checks using Node.js, Express, and PostgreSQL.
- Backend: Node.js, Express.js
- Database: PostgreSQL
- Database Driver:
pg - API Style: REST APIs
- Testing: Postman
- Database GUI: Beekeeper Studio
src/
│
├── controllers/
│ ├── product.controller.js
│ ├── metal.controller.js
│ └── diamond.controller.js
│
├── utils/
│ └── prodPriceCalc.js
│
├── routes/
│ ├── product.routes.js
│ ├── metal.routes.js
│ └── diamond.routes.js
│
├── middlewares/
│ └── globalErrorHandler.middleware.js
│
├── config/
│ └── db.js
|
├── db/
| ├── junction_tables/
| | ├── productDiamond.sql
| | ├── productMetal.sql
| | └── productRingSize.sql
| |
| |── diamond.sql
| |── inventory.sql
| |── metal.sql
| |── pricing.sql
| |── product.sql
| |── purityLevel.sql
| └── ringSize.sql
|
└── app.js
-
controllers/
Contains all request handling and database interaction logic. -
utils/
Holds reusable business logic (e.g., price calculation), keeping controllers clean. -
routes/
Responsible only for routing and mapping APIs to controllers. -
middlewares/
Centralized global error handling. -
config/
Database connection setup using PostgreSQL. -
db/
Pure SQL schema definitions:- Individual table schemas
- Junction tables for many-to-many relationships
- Easily executable in Beekeeper Studio or psql
git clone <repository-url>
npm install
Database Config:
// config/db.js
const { Pool } = require("pg");
const pool = new Pool({
host: `${DB_HOST_NAME}`,
user: `${DB_USER}`,
database: `${DB_NAME}`,
port: `${DB_PORT}`,
password: `${DB_PASSWORD}`,
});
module.exports = pool;Run the provided SQL CREATE TABLE scripts in Beekeeper Studio or psql.
npm run devNote: Add .env file with variables in .env.example in root directory
The jewellery product domain involves highly structured data and clear relationships between entities such as:
- Products
- Metals
- Purity levels
- Diamonds
- Inventory combinations
This makes PostgreSQL a better fit for the following reasons:
-
One-to-many and many-to-many relationships are clearly defined (e.g. Product ↔ Metal, Product ↔ Ring Size, Product ↔ Diamond).
-
Foreign keys ensure only valid combinations exist.
-
Jewellery pricing depends on precise data (metal purity, weight, tax, discounts).
-
PostgreSQL enforces ACID compliance, which guarantees accurate and reliable calculations.
-
Constraints like
NOT NULL,CHECK, andFOREIGN KEYprevent invalid or incomplete data. -
Inventory and pricing logic remain consistent across the system.
-
MongoDB allows flexible, schema-less data, which can lead to:
- Inconsistent or distorted data structures
- Duplication of pricing and inventory logic
-
Managing complex relationships and validations becomes cumbersome.
-
Enforcing strict pricing and inventory rules requires additional application-level checks.
Since this system requires structured schemas, strong relationships, and precise pricing calculations, PostgreSQL is more suitable than MongoDB for this Jewellery PDP backend system.
The final jewellery product price is calculated dynamically based on multiple factors to ensure accuracy and consistency.
-
Metal Type & Purity
- Base metal price per gram
- Purity percentage
- Example:
- 24K → 100%
- 18K → 75%
- Example:
-
Product Base Weight
- Weight of the metal used in the product (in grams)
-
Diamond Price
- Calculated using:
- Diamond carat
- Price per carat
- Calculated using:
-
Making Charges
- Fixed or configurable making cost for the product
-
Tax
- Percentage-based tax applied on subtotal
-
Discount (Optional)
- Exchange or promotional discount, if applicable
Metal_Price = base_weight × metal_price_per_gram × purity_percentage
Diamond_Price = diamond_carat × diamond_price_per_carat
Subtotal = Metal Price + Diamond Price + Making Charges
Tax = Subtotal × tax_percentage
Final_Price = Subtotal + Tax − Discount👉 All calculation logic is isolated inside utils/prodPriceCalc.js.
All APIs are REST-based and designed to support the Jewellery Product Detail Page (PDP) functionality, including product information, pricing calculation, and inventory availability.
Base URL: http://localhost:3000/api
Fetches all available jewellery products.
Endpoint
[GET] /products/Response
{
"success": true,
"data": [
{
"prod_id": 1,
"prod_name": "Gold Ring Classic",
"prod_description": "Classic 18K gold ring suitable for daily wear",
"prod_base_weight": "6.50",
"prod_making_charges": "1200.00",
"is_available": true,
"is_bis_hallmarked": true,
"is_gia_certified": false
},
{
"prod_id": 2,
"prod_name": "Diamond Engagement Ring",
"prod_description": "Elegant diamond ring for engagement",
"prod_base_weight": "4.20",
"prod_making_charges": "2500.00",
"is_available": true,
"is_bis_hallmarked": true,
"is_gia_certified": true
},
...
]
}Fetches details of a specific product.
Endpoint
[GET] /products/:prodIdResponse
{
"success": true,
"data": {
"prod_id": 5,
"prod_name": "Diamond Stud Earrings",
"prod_description": "Minimal diamond stud earrings",
"prod_base_weight": "3.10",
"prod_making_charges": "1800.00",
"is_available": true,
"is_bis_hallmarked": true,
"is_gia_certified": true
}
}Dynamically calculates the final price based on selected customization options.
Endpoint
[GET] /products/calcPriceRequest Body
{
"payload": {
"prodId": 2
}
}Response
{
"success": true,
"priceDetails": {
"metalCost": 1953000,
"purityFactor": "75%",
"diamondCost": 60000,
"makingCharges": 2500,
"basePrice": 2015500,
"taxAmount": 60465,
"exchangeDiscount": 1000,
"finalPrice": 2074965
}
}Checks whether a product is available for the selected configuration inside inventory
Query Parameters
| Parameter | Type |
|---|---|
| prodId | number |
| metalId | number |
| purityId | number |
| ringSizeId | number |
Endpoint
[GET] /products/availability?prodId=2&metalId=1&purityId=3&ringSizeId=4Response
{
"availability": true,
"quantity": 10
}Creates a new jewellery product entry inside postgres DB.
Endpoint
[POST] /products/createRequest Body
{
"name": "Rose Gold Pendant",
"description": "Rose gold heart-shaped pendant",
"baseWeight": 5.40,
"makingCharges": 1400.00,
"isBISHallmarked": "TRUE",
"isGIACertified": "FALSE"
}Response
{
"success": true,
"prodDetails": {
"prod_id": 8,
"prod_name": "Rose Gold Pendant",
"prod_description": "Rose gold heart-shaped pendant",
"prod_base_weight": "5.40",
"prod_making_charges": "1400.00",
"is_available": true,
"is_bis_hallmarked": true,
"is_gia_certified": false
}
}Creates a new metal info entry inside postgres DB.
Endpoint
[POST] /metals/createRequest Body
{
"name": "Silver",
"purity": "999",
"color": "White",
"pricePerGram": 85.0000,
"isAlloy": "FALSE",
"description": "Pure silver for coins"
}Response
{
"success": true,
"metalDetails": {
"metal_id": 20,
"metal_name": "Silver",
"metal_description": "Pure silver for coins",
"metal_purity": "999",
"metal_color": "White",
"metal_pricepergram": "85.0000",
"isalloy": false
}
}Fetches information of all metals.
Endpoint
[GET] /metals/Response
{
"success": true,
"data": [
{
"metal_id": 1,
"metal_name": "Gold",
"metal_description": "Pure gold used for coins and investment",
"metal_purity": "24K",
"metal_color": "Yellow",
"metal_pricepergram": "6200.0000",
"isalloy": false
},
{
"metal_id": 2,
"metal_name": "Gold",
"metal_description": "Popular for traditional jewellery",
"metal_purity": "22K",
"metal_color": "Yellow",
"metal_pricepergram": "5700.0000",
"isalloy": true
},
...
]
}Fetches details of a specific metal.
Endpoint
[GET] /metals/:metalIdResponse
{
"success": true,
"data": {
"metal_id": 5,
"metal_name": "Platinum",
"metal_description": "Premium metal for wedding bands",
"metal_purity": "950",
"metal_color": "White",
"metal_pricepergram": "3200.0000",
"isalloy": false
}
}Adds a new diamond configuration to a postgres DB.
Endpoint
[POST] /diamonds/createRequest Body
{
"carat": 1.50,
"quality": "SI2",
"pricePerCarat": 180000.00
}Response
{
"success": true,
"data": {
"diamond_id": 8,
"diamond_carat": "1.50",
"diamond_quality": "SI2",
"diamond_price_per_carat": "180000.00"
}
}Fetches information of all diamonds which are available.
Endpoint
[GET] /diamonds/Response
{
"success": true,
"data": [
{
"diamond_id": 1,
"diamond_carat": "0.30",
"diamond_quality": "VVS1",
"diamond_price_per_carat": "85000.00"
},
{
"diamond_id": 2,
"diamond_carat": "0.50",
"diamond_quality": "VVS2",
"diamond_price_per_carat": "120000.00"
},
...
]
}Fetches details of a specific diamond.
Endpoint
[GET] /diamonds/:diamondIdResponse
{
"success": true,
"data": {
"diamond_id": 4,
"diamond_carat": "1.00",
"diamond_quality": "VS2",
"diamond_price_per_carat": "220000.00"
}
}All APIs use a centralized global error handler middleware.
Usage
This errorHandler middleware is defined at last after defining all the routes.
app.use(globalErrorHandler)Sample Error Response
{
success: false,
message: 'Database operation failed'
}The system is designed to handle common real-world edge cases gracefully to ensure reliability and correct behaviour.
- All required query parameters and request body fields are validated at the API level.
- If any required parameter (e.g.,
prodId,metalId,purityId,ringSizeId) is missing or invalid, the API returns a clear400 Bad Requestresponse. - This prevents unnecessary database queries and avoids unexpected crashes.
Example
{
"status": false,
"message": "All selected parameters are required"
}Not all combinations of product, metal, purity, and ring size are valid.
For example
-
A specific ring size may not be available for a particular metal or purity.
-
A product may not support certain diamond or metal combinations.
-
The system checks the Inventory table to verify whether the selected combination exists.
-
If no matching inventory record is found, the API clearly informs the user that the selected configuration is not supported.
Error Response
if (inventoryRes.rows.length === 0) {
return res.status(400).json({
availablity: false,
message: "The combination of multiple inventory factors is not supported"
});
}-
Even if a customization combination exists, the product may be temporarily out of stock.
-
The inventory quantity is checked in real time.
-
If the quantity is 0 or less, the API responds with an Out of Stock message instead of allowing the purchase flow.
Example
if (quantity <= 0) {
return res.status(400).json({
availablity: false,
message: "Product is Out of Stock"
});
}-
Pricing is always calculated dynamically using the latest values stored in the database.
-
Metal prices, diamond prices, tax, and discounts are fetched at the time of calculation.
-
Since pricing is computed on-demand and not cached, stale pricing scenarios are naturally avoided.
-
Although a createdAt or versioning field is not implemented in this assignment (to keep scope minimal), dynamic calculation ensures price consistency.
Justification
For this assignment scope, real-time calculation using current database values is sufficient to prevent outdated pricing without introducing additional complexity.
To keep the implementation focused, clear, and aligned with the assignment scope, the following assumptions were made:
- Each row in the
Inventorytable represents a unique and valid combination of:- Product
- Metal type
- Purity level
- Ring size
- This design ensures accurate availability checks and avoids ambiguity while calculating stock.
Example:
A gold ring of 18K purity in size 7 is treated as a separate inventory entry from the same ring in size 8 or with a different purity.
-
Pricing components such as:
- Making charges
- Tax percentage
- Discount (if applicable)
-
are assumed to be product-specific and stored accordingly.
-
This allows flexible pricing control at the product level without duplicating logic.
-
Diamond pricing is calculated per carat.
-
The final diamond price is derived using:
- Selected diamond carat value
- Price per carat stored in the database
-
Only one diamond configuration per product is assumed for simplicity.
-
Authentication, authorization, payments, and frontend/UI implementation are out of scope for this assignment.
-
The focus is strictly on backend API design, data modelling, and pricing logic.
-
Emphasis is placed on:
- Clean API design
- Logical data modelling
- Accurate price calculation
- Inventory availability handling
-
Advanced architecture patterns, caching layers, or UI-level optimizations are intentionally excluded to keep the solution aligned with the problem statement.
These assumptions help maintain a clean, understandable backend system while accurately simulating a real-world jewellery product detail page (PDP) at a functional level.