A full-stack Express + PostgreSQL (Neon) bookstore inventory application featuring CRUD, image uploads, search, sorting, filtering, and clean server-rendered views using EJS + Tailwind CSS.
Built as part of The Odin Project, but structured like a production-grade MVC application.
Render: https://chapterhub-inventory.onrender.com/
Go to Live demo Section to see more pages - Error 500 Page, Edit Genre Page , Delete Book Page
- Create, edit, delete books
- Upload cover images (Multer)
- Metadata support: title, author, year, description
- Additional fields: price, quantity
- Filename hashing using crypto
- Full validation with express-validator
- Create, edit, delete genres
- Name + description fields
- Prevent duplicates using DB constraints
- Genre dropdown in book forms
- Search books by title
- Filter books by one or multiple genres
- Sort options:
- Title (AβZ / ZβA)
- Year (Oldest / Newest)
- Price (Low β High / High β Low)
- PostgreSQL hosted on Neon
- Parameterized SQL queries (SQL injection safe)
- Modular queries for books & genres
- Seed script includes:
- Schema generation
- Sample genres & books
--resetmode- Index creation for genres
- Tailwind CSS v4
- Modern, minimal, responsive UI
- Clean EJS templates
- Node.js
- Express 5
- Express Validator
- Multer (image uploads)
- PostgreSQL (Neon)
- EJS
- Tailwind CSS 4
- Vanilla JavaScript
- Nodemon
- Concurrently
- pnpm
- dotenv
chapterhub-inventory/
β
ββ app.js
ββ package.json
ββ nodemon.json
β
ββ db/
β ββ seed.js
β ββ pool.js
β ββ data/
β β ββ books.js
β β ββ genres.js
β ββ queries/
β ββ books.js
β ββ genres.js
β
ββ controllers/
β ββ booksController.js
β ββ genresController.js
β
ββ routes/
β ββ booksRouter.js
β ββ genresRouter.js
β ββ indexRouter.js
β
ββ validators/
β ββ booksValidators.js
β ββ genresValidators.js
β
ββ middlewares/
β ββ upload.js
β ββ setLocals.js
β
ββ views/
β
ββ public/
| Column | Type | Notes |
|---|---|---|
| id | identity (PK) | auto-generated |
| name | varchar(100) | unique, required |
| description | text | optional |
| created_at | timestamptz | defaults to NOW() |
| Column | Type | Notes |
|---|---|---|
| id | identity (PK) | auto-generated |
| title | varchar(255) | required |
| author | varchar(255) | optional |
| description | text | optional |
| year | integer | optional |
| genre_id | integer (FK) | references genres(id) |
| image_url | text | uploaded path |
| price | numeric(10,2) | default 0 |
| quantity | int | default 0 |
| created_at | timestamptz | defaults to NOW() |
| UNIQUE(title, author) | prevents duplicates |
π Index:
CREATE INDEX idx_books_genre_id ON books(genre_id);git clone https://github.com/devxsameer/chapterhub-inventory.git
cd chapterhub-inventorypnpm installDATABASE_URL=your_neon_connection_string
PORT=6969
Fresh reset:
pnpm seed:resetNormal seed:
pnpm seedpnpm devpnpm css:buildpnpm start
Server runs at:
http://localhost:6969
- Uploads stored in:
public/uploads/ - Uses Multer disk storage
- Accepts:
.jpg,.jpeg,.png,.webp - Max file size: 5MB
- Unique filenames using
crypto.randomBytes
Sameer Ali
Full-stack developer & Odin Project learner.
MIT License β free to modify & share.




