A full-stack library management system built with NestJS, Prisma, PostgreSQL, and React.
- Screenshots
- Features
- Tech Stack
- Getting Started
- Running Migrations and Seeding Data
- Testing Protected Routes
- API Routes
- Database Schema
- UI Flows
- Design Decisions & Assumptions
- Quick Start Summary
📸 Click to view screenshots
- Books Management: CRUD operations with search and filter (by title, author, availability)
- Authors Management: CRUD operations for authors
- User Management: Admin can view all users
- Borrowing System: Users can borrow and return books (14-day loan period)
- Role-based Access: Admin and User roles with protected routes
- JWT Authentication: Secure login with token-based auth
| Layer | Technology |
|---|---|
| Backend | NestJS, TypeScript, Prisma ORM |
| Database | PostgreSQL (Supabase / Local / Docker) |
| Frontend | React 18, TypeScript, Vite |
| Auth | JWT (passport-jwt), bcrypt |
| Docs | Swagger/OpenAPI |
- Node.js 18+
- npm or yarn
- PostgreSQL database (Supabase, local, or Docker)
Step 1: Create Supabase Project
- Go to supabase.com and create a free account
- Create a new project, set a database password
- Go to Settings → Database → Connection string → URI
- Copy the connection string
Step 2: Setup Backend
cd library-api
npm install
# Create environment file
cp .env.example .envEdit .env:
DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
JWT_SECRET="any-secret-string-here"# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma migrate dev --name init
# Seed demo data
npx prisma db seed
# Start server
npm run start:devStep 3: Setup Frontend
cd library-frontend
npm install
npm run devStep 1: Install PostgreSQL
# macOS
brew install postgresql
brew services start postgresql
# Ubuntu
sudo apt install postgresql
sudo systemctl start postgresqlStep 2: Create Database
psql postgres
CREATE DATABASE library_db;
\qStep 3: Setup Backend
cd library-api
npm install
cp .env.example .envEdit .env:
DATABASE_URL="postgresql://localhost:5432/library_db"
DIRECT_URL="postgresql://localhost:5432/library_db"
JWT_SECRET="any-secret-string-here"npx prisma generate
npx prisma migrate dev --name init
npx prisma db seed
npm run start:devStep 4: Setup Frontend
cd library-frontend
npm install
npm run devThe project includes a complete Docker setup that runs PostgreSQL, Backend API, and Frontend together.
Step 1: Setup Environment Variables
# Copy the example environment file
cp .env.example .envThe .env file contains:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_DB=library_db
JWT_SECRET=your_jwt_secret_hereStep 2: Start All Services
docker-compose up --buildThis starts:
- PostgreSQL on port 5432
- Backend API on http://localhost:3000
- Frontend on http://localhost:5173
The backend automatically runs migrations and seeds demo data on first start.
Step 3: Access the Application
- Frontend: http://localhost:5173
- API: http://localhost:3000/api
- Swagger Docs: http://localhost:3000/api/docs
Useful Docker Commands:
# Start in background
docker-compose up -d
# View logs
docker-compose logs -f
# Stop all services
docker-compose down
# Reset database (removes all data)
docker-compose down -v
docker-compose up --buildIf you prefer to run backend/frontend locally but use Docker for PostgreSQL:
Step 1: Start PostgreSQL
docker run -d \
--name library-db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres123 \
-e POSTGRES_DB=library_db \
-p 5432:5432 \
postgres:16-alpineStep 2: Setup Backend
cd library-api
npm install
cp .env.example .envEdit .env:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/library_db"
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/library_db"
JWT_SECRET="any-secret-string-here"npx prisma generate
npx prisma migrate dev --name init
npx prisma db seed
npm run start:devStep 3: Setup Frontend
cd library-frontend
npm install
npm run devcd library-api
npx prisma generate# Create new migration (after schema changes)
npx prisma migrate dev --name <migration-name>
# Apply migrations to production
npx prisma migrate deploynpx prisma db seedThis creates:
- Admin user: admin@library.com / admin123
- Regular users: john@example.com, jane@example.com (password: user123)
- 5 authors with biographies
- 10 books across different genres
- Sample borrowings
npx prisma studioOpens at http://localhost:5555
npx prisma migrate resetThis drops all data, re-runs migrations, and re-seeds.
Using cURL:
# Login as admin
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@library.com", "password": "admin123"}'
# Response:
# {"access_token": "eyJhbGciOiJIUzI1NiIs..."}Using Swagger UI:
- Open http://localhost:3000/api/docs
- Go to Auth → POST /api/auth/login
- Click "Try it out"
- Enter:
{"email": "admin@library.com", "password": "admin123"} - Copy the
access_tokenfrom response
Using cURL:
# Get all users (admin only)
curl http://localhost:3000/api/users \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# Borrow a book
curl -X POST http://localhost:3000/api/borrowings/borrow \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"bookId": "BOOK_UUID_HERE"}'
# Get my borrowings
curl http://localhost:3000/api/borrowings/my \
-H "Authorization: Bearer YOUR_TOKEN_HERE"Using Swagger UI:
- Click "Authorize" button (top right)
- Enter:
Bearer YOUR_TOKEN_HERE - Click "Authorize"
- Now all protected endpoints will include the token
| Account | Password | Can Access | |
|---|---|---|---|
| Admin | admin@library.com | admin123 | Everything |
| User | john@example.com | user123 | Books, Authors, Own Borrowings |
Admin-only endpoints:
- GET
/api/users- List all users - GET
/api/borrowings- All borrowings - POST/PATCH/DELETE
/api/books- Manage books - POST/PATCH/DELETE
/api/authors- Manage authors
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/auth/register |
Register new user | Public |
| POST | /api/auth/login |
Login, returns JWT | Public |
| GET | /api/auth/me |
Get current user | Required |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/books |
List books (with filters) | Public |
| GET | /api/books/:id |
Get book details | Public |
| POST | /api/books |
Create book | Admin |
| PATCH | /api/books/:id |
Update book | Admin |
| DELETE | /api/books/:id |
Delete book | Admin |
Filters: ?search=gatsby&authorId=uuid&available=true
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/authors |
List all authors | Public |
| GET | /api/authors/:id |
Get author + books | Public |
| POST | /api/authors |
Create author | Admin |
| PATCH | /api/authors/:id |
Update author | Admin |
| DELETE | /api/authors/:id |
Delete author | Admin |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/users |
List all users | Admin |
| GET | /api/users/:id |
Get user details | Required |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/borrowings/borrow |
Borrow a book | Required |
| POST | /api/borrowings/return/:id |
Return a book | Required |
| GET | /api/borrowings/my |
My borrowings | Required |
| GET | /api/borrowings |
All borrowings | Admin |
enum Role {
USER
ADMIN
}
enum BorrowStatus {
BORROWED
RETURNED
OVERDUE
}
model User {
id String @id @default(uuid())
email String @unique
password String // bcrypt hashed
name String
role Role @default(USER)
borrowings Borrowing[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Author {
id String @id @default(uuid())
name String
biography String?
books Book[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Book {
id String @id @default(uuid())
title String
isbn String @unique
authorId String
author Author @relation(fields: [authorId], references: [id])
quantity Int @default(1) // Total copies
availableQty Int @default(1) // Available copies
borrowings Borrowing[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Borrowing {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
bookId String
book Book @relation(fields: [bookId], references: [id])
borrowDate DateTime @default(now())
dueDate DateTime // borrowDate + 14 days
returnDate DateTime? // null until returned
status BorrowStatus @default(BORROWED)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Home Page → Books (browse/search/filter) → Authors (view all)
Login → Home → Books → Click "Borrow" → My Borrowings → Click "Return"
Login → Home → Books (Add/Edit/Delete) → Authors (Add/Edit/Delete)
→ Users (view all) → All Borrowings (view all)
| Route | Access | Description |
|---|---|---|
/ |
Public | Home page |
/books |
Public | Browse books |
/authors |
Public | Browse authors |
/login |
Public | Login page |
/register |
Public | Register page |
/my-borrowings |
User | My borrowed books |
/users |
Admin | All users |
/all-borrowings |
Admin | All borrowings |
-
Borrowing Duration: Fixed at 14 days (hardcoded). A real system would make this configurable.
-
ISBN Format: Stored as simple string without validation. Real systems would validate ISBN-10/13 format.
-
Single Copy Borrowing: Users can borrow multiple different books but only one copy of each book at a time.
-
No Reservations: If a book is unavailable, users must wait. No reservation queue implemented.
-
No Fines: Overdue books don't incur fines. Status changes to OVERDUE but no monetary penalty.
-
Email Uniqueness: Email is the unique identifier for users. No username field.
| Decision | Choice | Reasoning |
|---|---|---|
| availableQty on Book | Store directly | Faster reads vs calculating from borrowings each time |
| JWT Expiry | 7 days | Simpler for demo; production would use 15-60 min + refresh tokens |
| Password Hashing | bcrypt (10 rounds) | Industry standard, good security/performance balance |
| State Management | React Context | Simple app, only auth state is global; avoids Redux complexity |
| Role System | Simple enum | Only 2 roles needed; avoids over-engineering permissions |
| API Prefix | /api |
Clear separation, easy to proxy in production |
| Validation | class-validator + ValidationPipe | NestJS native, automatic DTO validation |
| Database | PostgreSQL | Relational data with clear relationships; Prisma ORM support |
- Passwords hashed with bcrypt before storage
- JWT tokens required for protected routes
- Role-based guards on admin endpoints
- CORS configured for frontend origin
- Input validation on all endpoints
- Refresh token rotation for better security
- Rate limiting on auth endpoints
- Email verification on registration
- Password reset functionality
- Audit logging for admin actions
- Pagination on list endpoints
- Full-text search with PostgreSQL
- Automated overdue status updates (cron job)
# Terminal 1 - Backend
cd library-api
npm install
cp .env.example .env # Edit with your DB connection
npx prisma generate
npx prisma migrate dev --name init
npx prisma db seed
npm run start:dev # http://localhost:3000
# Terminal 2 - Frontend
cd library-frontend
npm install
npm run dev # http://localhost:5173Demo Login:
- Admin: admin@library.com / admin123
- User: john@example.com / user123
Swagger Docs: http://localhost:3000/api/docs






