REST API for Brew Inventory, a full-stack inventory management app for a specialty tea and coffee store. Built with Node.js, Express, and PostgreSQL.
Live: brew-inventory.pages.dev · API: brew-inventory-server.onrender.com
Note: The backend is hosted on Render's free tier, which spins down after inactivity. The first request after idle may take 30–50 seconds while the server wakes up. Subsequent requests are fast.
brew-inventory-client — React frontend
- RESTful API for products and categories
- EAV (Entity-Attribute-Value) schema to support per-category product attributes
- Request validation with express-validator, including conditional attribute validation per category
- Database transactions for multi-step write operations, with rollback on partial failure
- Password-protected destructive actions (update/delete) for products
- Cascading deletes across the category → product → attribute relationship
- Unit and integration tests with Jest and Supertest
- Node.js with Express
- PostgreSQL with the
pgdriver - express-validator for request validation
- Jest for unit testing, Supertest for HTTP-level integration tests
- dotenv for environment configuration
└── 📁brew-inventory-server
└── 📁controllers
├── categoriesController.js
├── productsController.js
└── 📁db
├── categoryQueries.js
├── init.js
├── pool.js
├── productQueries.js
├── seed.js
├── seedDummyData.js
└── 📁middleware
└── 📁validators
├── productValidators.js
└── 📁routes
├── categoriesRouter.js
├── productsRouter.js
└── 📁tests
└── 📁controllers
├── categoriesController.test.js
├── productsController.test.js
└── 📁integration
└── 📁fixtures
├── productRows.js
├── categoriesRouter.test.js
├── categoryQueries.test.js
├── productQueries.test.js
├── productsRouter.test.js
└── 📁middleware
└── 📁validators
├── productValidators.test.js
├── globalSetup.js
├── loadTestEnv.js
├── setupPerFile.js
├── .gitignore
├── app.js
├── jest.config.js
├── package-lock.json
├── package.json
└── README.md
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/categories |
Get all categories |
| GET | /api/categories/:id |
Get a single category |
| POST | /api/categories |
Create a category |
| PUT | /api/categories/:id |
Update a category |
| DELETE | /api/categories/:id |
Delete a category |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/products |
Get all products (supports ?category_id=, ?availability=, ?search=) |
| GET | /api/products/:id |
Get a single product |
| POST | /api/products |
Create a product |
| PUT | /api/products/:id |
Update a product |
| DELETE | /api/products/:id |
Delete a product |
Four tables: categories, products, attributes, and product_attributes.
The app uses an EAV pattern where each category defines its own set of attributes (e.g. Coffee has Origin, Roast Level, Format, Weight) stored in the attributes table, with per-product values stored in product_attributes. This keeps the products table clean and avoids sparse nullable columns.
Cascading deletes: products.category_id and attributes.category_id both reference categories.id with ON DELETE CASCADE, and product_attributes.product_id cascades from products.id. Deleting a category deletes all of its products and their attributes. Category deletion isn't exposed in the frontend yet. This is backend-only groundwork for a future admin feature, which would warn the user before deleting a category with existing products.
Attribute filtering (GET /api/products?<Attribute Name>=value) originally interpolated the attribute name directly into SQL, which was a SQL injection vector. Fixed by parameterizing it like any other value. Covered by integration tests confirming that malicious input is treated as a literal value with no matches, rather than executed as SQL.
- Node.js
- PostgreSQL
- Clone the repository
git clone https://github.com/Maddily/brew-inventory-server.git
cd brew-inventory-server- Install dependencies
npm install- Create a
.envfile in the root directory
DATABASE_URL=postgresql://username:password@localhost:5432/brew_inventory
PORT=3000
ADMIN_PASSWORD=brew123
- Set up the database
node db/init.js
node db/seed.jsOptionally, seed the database with sample products:
node db/seedDummyData.js- Start the server
npm startThe API will be running at http://localhost:3000.
- Database: Neon (serverless PostgreSQL)
- API: Render
- Frontend: Cloudflare Pages
The API reads DATABASE_URL, ADMIN_PASSWORD, and FRONTEND_URL (for CORS) from environment variables set in Render's dashboard. The frontend's VITE_API_URL is set at build time in Cloudflare Pages and baked into the production bundle.
Unit tests:
npm testIntegration tests (requires a test database - see below):
npm run test:integrationThe test suite is split into two layers:
- Unit tests (
tests/controllers,tests/middleware) - controller logic tested with mocked query layers and mocked express-validator, covering success cases, error handling, and validation failures. Validator middleware chains are tested in isolation. - Integration tests (
tests/integration) - real HTTP requests via Supertest against the actual Express app, backed by a separate test PostgreSQL database. These cover query correctness (filtering, joins, transactions, cascading deletes), full request/response cycles through router → validation → controller → database, and password-protected routes.
Integration tests use a separate test database so they don't affect your development data.
- Create a test database:
psql -U your_user -c "CREATE DATABASE brew_inventory_test;"- Create a
.env.testfile:
DATABASE_URL=postgresql://username:password@localhost:5432/brew_inventory_test
ADMIN_PASSWORD=testpassword123
Integration tests automatically reset and reseed the test database before each test.