Skip to content

luke-dowling/record-store-api

Repository files navigation

Record Store API

A RESTful API for managing vinyl record inventory built with NestJS, Sequelize, and PostgreSQL. Features include full CRUD operations, normalized database design, advanced filtering, pagination, security middleware, and Docker support.

🚀 Features

  • CRUD Operations - Create, read, update, and delete vinyl records, genres, and artists
  • Authentication & Authorization - JWT-based user authentication with protected routes
  • Advanced Filtering - Search records by genre, artist, album name, price range, and release year
  • Pagination - Efficient pagination with configurable limits (default: 10, max: 50)
  • Security - Helmet middleware, rate limiting (10 req/min), CORS, password hashing, and input validation
  • API Documentation - Interactive Swagger UI at /docs with Bearer token support
  • Database Seeding - Pre-populated with 15 classic albums, 8 genres, and 13 artists
  • Docker Support - Containerized development environment with hot reload
  • Type Safety - Full TypeScript implementation
  • Testing - Unit and E2E test suites
  • Environment Configuration - Flexible environment-based config

📋 Prerequisites

  • Node.js 20+
  • Bun 1+ (package manager/runtime)
  • Docker (optional, for containerized setup)
  • PostgreSQL (if running without Docker)

🛠️ Tech Stack

  • Framework: NestJS
  • ORM: Sequelize with sequelize-typescript
  • Database: PostgreSQL
  • Runtime: Bun
  • Authentication: JWT (JSON Web Tokens) with Passport
  • Password Hashing: bcrypt
  • Validation: class-validator
  • Documentation: Swagger/OpenAPI
  • Testing: Jest + Supertest

⚙️ Configuration

Create a .env file in the project root (use .env.example as a template):

# Database
DB_HOST=localhost
DB_PORT=5434
DB_USERNAME=dev-user
DB_PASSWORD=dev-password
DB_DATABASE=record_store_db

# Application
NODE_ENV=development
CORS_ORIGIN=http://localhost:3000

# JWT Authentication
JWT_SECRET=your-secret-key-change-in-production-use-long-random-string
JWT_REFRESH_SECRET=your-refresh-secret-key-change-in-production-use-different-long-random-string
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# Docker Postgres
POSTGRES_USER=dev-user
POSTGRES_PASSWORD=dev-password
POSTGRES_DB=record_store_db

🐳 Getting Started with Docker (Recommended)

The easiest way to get started. Includes PostgreSQL and the API with hot reload.

# Start both database and API
docker compose up --build

# Or start only the database and run the API locally with bun
docker compose up db

Access:

Stop:

docker compose down        # Stop containers
docker compose down -v     # Stop and remove volumes

💻 Getting Started Locally

Run the API on your host machine with a local or Docker PostgreSQL instance.

# Install dependencies
bun install

# Start PostgreSQL (if using Docker for DB only)
docker compose up db

# Run database seeders
bun run seed

# Start development server with hot reload
bun run dev

Access:

📜 Available Scripts

Script Description
bun run dev Start development server with hot reload
bun run start Start production server
bun run build Compile TypeScript to dist/
bun run start:prod Run compiled production build
bun run test Run unit tests
bun run test:e2e Run end-to-end tests
bun run test:watch Run tests in watch mode
bun run test:cov Run tests with coverage
bun run seed Populate database with sample records
bun run lint Lint and fix code
bun run format Format code with Prettier

🗂️ API Endpoints

Root

  • GET /api - API info and available endpoints

Records

  • GET /api/records - Get all records (paginated, with optional filters)
    • Query params: page, limit, genre, artist, album, minPrice, maxPrice, year
    • Example: /api/records?genre=rock&maxPrice=30&page=1&limit=10
  • GET /api/records/:id - Get a single record by UUID (includes artist and genre details)
  • POST /api/records - Create a new record (requires artistId and genreId)
  • PATCH /api/records/:id - Update a record
  • DELETE /api/records/:id - Delete a record

Genres

  • GET /api/genres - Get all genres
  • GET /api/genres/:id - Get a single genre by UUID
  • POST /api/genres - Create a new genre
  • PATCH /api/genres/:id - Update a genre
  • DELETE /api/genres/:id - Delete a genre

Artists

  • GET /api/artists - Get all artists
  • GET /api/artists/:id - Get a single artist by UUID
  • POST /api/artists - Create a new artist
  • PATCH /api/artists/:id - Update an artist
  • DELETE /api/artists/:id - Delete an artist

Authentication

  • POST /api/auth/register - Register a new user
    • Body: { email, username, password, firstName?, lastName? }
    • Returns: Access token (15m) and refresh token (7d, in HttpOnly cookie)
    • Password requirements: Min 8 characters, must include uppercase, lowercase, number, and special character
  • POST /api/auth/login - Login with email and password
    • Body: { email, password }
    • Returns: Access token (15m) and refresh token (7d, in HttpOnly cookie)
  • GET /api/auth/refresh - Get new access token using refresh token
    • Automatically sends refresh token from cookie
    • Returns: New access token (15m)
  • POST /api/auth/logout - Logout and clear refresh token
    • Header: Authorization: Bearer <token>
    • Clears refresh token from cookie

Users

  • GET /api/users/profile - Get current user profile (requires authentication)
    • Header: Authorization: Bearer <access_token>

Documentation

  • GET /docs - Interactive Swagger UI

🔍 Search & Filter Examples

# Get records under $30
GET /api/records?maxPrice=30

# Search for jazz records
GET /api/records?genre=jazz

# Find records by Pink Floyd
GET /api/records?artist=pink

# Complex filter: Rock albums from the 70s under $35
GET /api/records?genre=rock&minPrice=20&maxPrice=35&year=1973

# Pagination: Get page 2 with 5 records per page
GET /api/records?page=2&limit=5

🔐 Authentication Flow

Token Strategy:

  • Access Token: Short-lived (15 minutes), sent in response body for API requests
  • Refresh Token: Long-lived (7 days), stored in secure HttpOnly cookie for automatic refresh
# 1. Register a new user (returns access token + refresh token in cookie)
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{
    "email": "john@example.com",
    "username": "johndoe",
    "password": "SecurePass123!"
  }'

# 2. Use access token for protected endpoints
curl -X GET http://localhost:3000/api/users/profile \
  -H "Authorization: Bearer <access_token>"

# 3. When access token expires (15m), refresh it using the cookie
curl -X GET http://localhost:3000/api/auth/refresh \
  -b cookies.txt
# Returns new access token

# 4. Logout (clears refresh token)
curl -X POST http://localhost:3000/api/auth/logout \
  -H "Authorization: Bearer <access_token>" \
  -b cookies.txt

🔒 Security Features

  • JWT Authentication: Dual-token strategy with short-lived access tokens and long-lived refresh tokens
    • Access tokens: 15-minute expiration (prevent exposure window)
    • Refresh tokens: 7-day expiration (revocable in database)
  • HttpOnly Cookies: Refresh tokens stored in secure, HttpOnly cookies (XSS-safe)
  • SameSite Protection: Cookies set to SameSite=strict for CSRF protection
  • Secure Flag: Cookies only sent over HTTPS in production
  • Password Hashing: bcrypt with salt rounds for secure password storage
  • Route Protection: JWT guards for protected endpoints
  • Helmet: Security headers (CSP, HSTS, X-Frame-Options, etc.)
  • Rate Limiting: 10 requests per minute per IP address
  • CORS: Configurable with credentials support for cookie-based auth
  • Input Validation: Automatic DTO validation and sanitization with strong password requirements
  • SQL Injection Protection: Sequelize ORM with parameterized queries

📄 License

MIT

About

High-performance REST API for vinyl record inventory management. Built with NestJS, featuring normalized data architecture, complex filtering, pagination, security hardening (Helmet, rate limiting), and Swagger documentation.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors