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.
- 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
/docswith 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
- Node.js 20+
- Bun 1+ (package manager/runtime)
- Docker (optional, for containerized setup)
- PostgreSQL (if running without Docker)
- 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
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_dbThe 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 dbAccess:
- API: http://localhost:3000/api
- Swagger Docs: http://localhost:3000/docs
- PostgreSQL: localhost:5434 (from host)
Stop:
docker compose down # Stop containers
docker compose down -v # Stop and remove volumesRun 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 devAccess:
- API: http://localhost:3000/api
- Swagger Docs: http://localhost:3000/docs
| 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 |
GET /api- API info and available endpoints
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
- Query params:
GET /api/records/:id- Get a single record by UUID (includes artist and genre details)POST /api/records- Create a new record (requiresartistIdandgenreId)PATCH /api/records/:id- Update a recordDELETE /api/records/:id- Delete a record
GET /api/genres- Get all genresGET /api/genres/:id- Get a single genre by UUIDPOST /api/genres- Create a new genrePATCH /api/genres/:id- Update a genreDELETE /api/genres/:id- Delete a genre
GET /api/artists- Get all artistsGET /api/artists/:id- Get a single artist by UUIDPOST /api/artists- Create a new artistPATCH /api/artists/:id- Update an artistDELETE /api/artists/:id- Delete an artist
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
- Body:
POST /api/auth/login- Login with email and password- Body:
{ email, password } - Returns: Access token (15m) and refresh token (7d, in HttpOnly cookie)
- Body:
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
- Header:
GET /api/users/profile- Get current user profile (requires authentication)- Header:
Authorization: Bearer <access_token>
- Header:
GET /docs- Interactive Swagger UI
# 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=5Token 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- 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=strictfor 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
MIT