A full-stack vinyl record store where users can browse albums, filter by genre, search by title or artist, and manage a shopping cart. link here
Spiral Sounds is a classic vinyl record shop built as a full-stack web application. The main goal was to learn how to:
- Build a REST API with Node.js and Express
- Manage sessions for user authentication using
express-session - Hash and verify passwords securely with bcryptjs
- Work with a SQLite relational database using the
sqliteandsqlite3packages - Validate and sanitise user input on both the client and server
- Structure a project with routes, controllers, and middleware separation
- Write vanilla JavaScript ES modules on the frontend
- Handle authenticated requests and protect routes with custom middleware
- Build a dynamic UI that reflects auth state (greeting, menu items, cart icon)
-
Clone the repository:
git clone https://github.com/your-username/spiral-sounds.git cd spiral-sounds -
Install dependencies:
npm install
-
(Optional) Set a session secret via environment variable — otherwise a default is used:
SPIRAL_SESSION_SECRET=your-secret-hereFor easy generate secret key use this code in terminal :
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -
Start the server:
npm start
-
Open your browser and go to
http://localhost:8000
npm start // Starts the Express server on port 8000
Note: This project uses plain Node.js with Express — no build step or bundler required. JS files are served as static ES modules directly from the
public/folder.
Session cookie not sent with fetch requests - After setting up express-session, the auth worked on login but every subsequent fetch to a protected route returned 401. The session cookie existed in the browser but wasn't being sent. The fix was adding credentials: 'include' to every fetch call that needed auth - without it the browser strips the cookie from cross-context requests by default. Also required setting sameSite: 'lax' on the session cookie to allow it through.
Static files conflicting with API routes - express.static('public') was catching requests before they reached the API routers because of middleware ordering. Moved app.use(express.static(...)) after all app.use('/api/...') registrations so API routes take priority, with static file serving as the fallback.
Cart total calculation across quantity - The cart table stores individual items with a quantity column. The total had to be computed from price × quantity per item, then summed - getting this right in both the SQL query and the frontend reduce took a few iterations to keep consistent between what the server returned and what the UI displayed.
Session-based auth over JWT - Sessions store auth state server-side with an httpOnly cookie, so the token is never accessible to JavaScript. For a server-rendered vanilla JS app with no separate frontend domain this is simpler and more secure than managing JWTs in localStorage.
Vanilla JS ES modules on the frontend - No bundler, no framework. Each page loads only the modules it needs (cart.js imports from cartService.js, index.js imports from productService.js). This kept the project focused on understanding the browser's native module system before reaching for build tools.
Route → controller separation - Routes define the paths and attach middleware; controllers contain the actual logic. This made it straightforward to add the requireAuth middleware to specific routes without touching the business logic.
- Hash passwords with bcrypt before storing — currently passwords may be stored in plain text in the SQLite database
- Add
secure: trueto the session cookie for production (requires HTTPS) - Replace the
innerHTMLapproach inrenderProductsandrenderCartItemswith DOM methods to reduce XSS risk
- Node.js v18 or higher
- A text editor
- A REST client
| No | File Name | What it does |
|---|---|---|
| 1 | server.js |
Entry point — sets up Express, sessions, static files, and mounts all routers |
| 2 | db/db.js |
Opens and returns a SQLite database connection |
| 3 | routes/auth.js |
Defines routes for register, login, and logout |
| 4 | routes/cart.js |
Defines routes for cart operations (add, get, delete) |
| 5 | routes/products.js |
Defines routes for fetching products and genres |
| 6 | routes/me.js |
Defines route for fetching the current logged-in user |
| 7 | controllers/authController.js |
Handles registration, login, and logout logic |
| 8 | controllers/cartController.js |
Handles add, fetch, and delete cart item logic |
| 9 | controllers/productsController.js |
Handles product and genre queries with optional filters |
| 10 | controllers/meController.js |
Returns current user info from session |
| 11 | middleware/requireAuth.js |
Blocks unauthenticated access to protected routes |
| 12 | public/js/index.js |
Main script for the homepage — initialises products, auth, and events |
| 13 | public/js/cart.js |
Main script for the cart page |
| 14 | public/js/authUI.js |
Shared auth helpers: session check, greeting, menu visibility |
| 15 | public/js/cartService.js |
Cart API calls and DOM rendering for cart items |
| 16 | public/js/productService.js |
Fetches products and populates the genre dropdown |
| 17 | public/js/productUI.js |
Renders product cards and handles search filtering |
| 18 | public/js/login.js |
Handles login form submission |
| 19 | public/js/signup.js |
Handles signup form submission |
| 20 | public/js/logout.js |
Sends logout request and redirects |
| 21 | public/js/menu.js |
Toggles the mobile nav menu |
| 22 | public/css/index.css |
All styles — variables, layout, components, responsive |
| 23 | database.db |
SQLite database with users, products, and cart_items tables |
| 24 | logTable.js |
Dev utility to log any DB table to the console |
There is no build step. The frontend uses native ES modules loaded directly by the browser. The Express server serves all static files from the public/ folder and handles API requests under /api/.
When a user visits the site:
- The browser loads the HTML and its associated JS module
- The JS fetches data from the API (products, auth status, cart count)
- The DOM is updated dynamically based on the response
Authentication is session-based — on login or register, a session is created server-side and a cookie is sent to the browser. Protected routes check req.session.userId via the requireAuth middleware.
- Found a bug? Open an issue and I'll try to fix it.
- Advice? If you have ideas for improving the store, the UI, or the API design, let me know!
Feel free to use this for your own practice! MIT License.