Add comprehensive project documentation including changelog, QA tracker, code quality audit, implementation guide, K6 load testing guide, frontend exploration notes, and file mapping reference. Co-Authored-By: Paperclip <noreply@paperclip.ing>
22 KiB
GoodGo Platform API — K6 Load Testing Guide
🎯 Quick Summary
Base URL: http://localhost:3001/api/v1
Node Version: >= 22.0.0
Testing Framework: Playwright (E2E), Vitest (Unit)
No existing K6 or load testing setup found
📋 Project Structure
Root Directory
goodgo-platform/
├── apps/api # NestJS backend (port 3001)
├── apps/web # Next.js 14 frontend (port 3000)
├── libs/mcp-servers # MCP tool server library
├── prisma/ # Database schema & migrations
├── e2e/ # Playwright E2E tests (api + web)
├── turbo.json # Turborepo config
├── package.json # Root workspace scripts
├── .env.example # Environment variables template
└── playwright.config.ts # Playwright configuration
Key Scripts (package.json)
pnpm dev # Start all apps (API :3001, Web :3000)
pnpm test # Unit tests via Vitest (API only)
pnpm test:e2e # Playwright E2E tests
pnpm test:e2e:api # API E2E tests only
pnpm test:e2e:web # Web E2E tests only
pnpm build # Production build
pnpm lint # ESLint
pnpm typecheck # TypeScript checking
🏗️ API Module Structure
API Base Architecture: apps/api/src/modules/
Each module follows DDD layers: domain/ → application/ → infrastructure/ → presentation/
modules/
├── auth/ # Authentication & JWT
├── listings/ # Property listings CRUD
├── payments/ # Payment processing (VNPay, MoMo, ZaloPay)
├── search/ # Full-text & geo search (Typesense)
├── subscriptions/ # Plans, quotas, usage tracking
├── admin/ # Moderation, KYC, user management
├── analytics/ # Market data, heatmaps, price trends
├── reviews/ # User reviews
├── notifications/ # Email, push (FCM), in-app
├── metrics/ # Prometheus metrics
├── health/ # Health checks
├── shared/ # Domain primitives, guards, pipes, logging
└── mcp/ # MCP tool server endpoints
🔐 AUTH MODULE
Controllers & Endpoints
File: apps/api/src/modules/auth/presentation/controllers/auth.controller.ts
| Method | Endpoint | Rate Limit | Auth | Description |
|---|---|---|---|---|
| POST | /auth/register |
5/hour | No | Register new user |
| POST | /auth/login |
5/hour | LocalAuth | Login with phone + password |
| POST | /auth/refresh |
5/hour | No | Refresh access token |
| POST | /auth/logout |
No limit | No | Clear auth cookies |
| POST | /auth/exchange-token |
No limit | No | Exchange OAuth tokens for cookies |
| GET | /auth/profile |
No limit | JWT | Get current user profile |
| GET | /auth/profile/agent |
No limit | JWT | Get agent profile for user |
| PATCH | /auth/kyc |
No limit | JWT+Admin | Verify user KYC (admin only) |
DTOs
LoginDto
{
phone: string // Required, example: "0901234567"
password: string // Required, example: "P@ssw0rd!"
}
RegisterDto
{
phone: string // Required, example: "0901234567"
password: string // Required, min 8 chars, example: "P@ssw0rd!"
fullName: string // Required, example: "Nguyen Van A"
email?: string // Optional, valid email format
}
RefreshTokenDto
{
refreshToken?: string // Optional if using cookie
}
VerifyKycDto
{
userId: string
kycStatus: string
kycData?: object
}
Cookies & Authentication
Access Token:
- Cookie:
access_token - Max Age: 15 minutes (900s)
- HttpOnly: true
- Secure: true (production only)
- SameSite: strict
- Path: /
Refresh Token:
- Cookie:
refresh_token - Max Age: 30 days
- HttpOnly: true
- Secure: true (production only)
- SameSite: strict
- Path:
/auth
Session Indicator:
- Cookie:
goodgo_authenticated= "1" - HttpOnly: false (visible to frontend)
OAuth Support
- Google OAuth 2.0
- Zalo OAuth (Vietnamese platform)
- Environment:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,ZALO_APP_ID,ZALO_APP_SECRET
🏠 LISTINGS MODULE
Controllers & Endpoints
File: apps/api/src/modules/listings/presentation/controllers/listings.controller.ts
| Method | Endpoint | Auth | Quota | Description |
|---|---|---|---|---|
| POST | /listings |
JWT | Yes | Create new listing |
| GET | /listings |
No | No | Search/filter listings (public) |
| GET | /listings/:id |
No | No | Get listing detail |
| GET | /listings/pending |
JWT+Admin | No | Get listings pending moderation |
| PATCH | /listings/:id/status |
JWT | No | Update listing status |
| POST | /listings/:id/media |
JWT | No | Upload photo/video |
| PATCH | /listings/:id/moderate |
JWT+Admin | No | Moderate a listing (admin) |
DTOs
CreateListingDto
{
transactionType: 'SALE' | 'RENT',
priceVND: bigint | string,
propertyType: 'APARTMENT' | 'HOUSE' | 'LAND' | etc.,
title: string, // Min 5 chars
description: string, // Min 10 chars
address: string,
ward: string,
district: string,
city: string,
latitude: number, // -90 to 90
longitude: number, // -180 to 180
areaM2: number, // Total area
usableAreaM2?: number,
bedrooms?: number,
bathrooms?: number,
floors?: number, // For houses
floor?: number, // For apartments
totalFloors?: number,
direction?: 'EAST' | 'WEST' | 'NORTH' | 'SOUTH' | etc.,
yearBuilt?: number,
legalStatus?: string,
amenities?: string[], // e.g., ['Hồ bơi', 'Gym']
nearbyPOIs?: object, // e.g., { schools: [], hospitals: [] }
metroDistanceM?: number,
projectName?: string,
agentId?: string,
rentPriceMonthly?: bigint | string,
commissionPct?: number,
}
SearchListingsDto
{
status?: 'ACTIVE' | 'INACTIVE' | 'ARCHIVED',
transactionType?: 'SALE' | 'RENT',
propertyType?: 'APARTMENT' | 'HOUSE' | 'LAND' | etc.,
city?: string,
district?: string,
minPrice?: bigint | string,
maxPrice?: bigint | string,
minArea?: number,
maxArea?: number,
bedrooms?: number,
page?: number, // Default: 1
limit?: number, // Default: 20, Max: 100
}
UpdateListingStatusDto
{
status: string,
moderationNotes?: string,
}
ModerateListingDto
{
action: 'APPROVE' | 'REJECT',
moderationScore?: number,
notes?: string,
}
Response Structures
ListingDetailData
Contains full listing information including:
- id, title, description
- propertyType, transactionType
- address, latitude, longitude, ward, district, city
- priceVND, rentPriceMonthly
- areaM2, usableAreaM2, bedrooms, bathrooms, floors
- amenities, nearbyPOIs
- legalStatus, yearBuilt, direction
- mediaUrls (photos/videos)
- agentInfo
- createdAt, updatedAt
PaginatedResult
{
items: ListingSearchItem[],
total: number,
page: number,
limit: number,
totalPages: number,
}
💳 PAYMENTS MODULE
Controllers & Endpoints
File: apps/api/src/modules/payments/presentation/controllers/payments.controller.ts
| Method | Endpoint | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /payments |
JWT | No | Create payment |
| GET | /payments |
JWT | No | List user transactions |
| GET | /payments/:id |
JWT | No | Get payment status |
| POST | /payments/callback/:provider |
No | 20/min | Handle payment callback (webhook) |
| POST | /payments/:id/refund |
JWT+Admin | No | Refund payment (admin) |
DTOs
CreatePaymentDto
{
provider: 'VNPAY' | 'MOMO' | 'ZALOPAY',
type: 'LISTING_FEE' | 'SUBSCRIPTION' | 'AGENT_COMMISSION',
amountVND: number, // 1 to 100,000,000,000
description: string, // Payment description
returnUrl: string, // URL (must be valid)
transactionId?: string, // External ID
idempotencyKey?: string, // For idempotency
}
ListTransactionsDto
{
status?: string,
limit?: number,
offset?: number,
}
RefundPaymentDto
{
reason: string,
}
Payment Providers
-
VNPay (Primary for Vietnam)
- Environment:
VNPAY_TMN_CODE,VNPAY_HASH_SECRET - Sandbox:
https://sandbox.vnpayment.vn/paymentv2/vpcpay.html - API:
https://sandbox.vnpayment.vn/merchant_webapi/api/transaction
- Environment:
-
MoMo (Mobile wallet)
- Environment:
MOMO_PARTNER_CODE,MOMO_ACCESS_KEY,MOMO_SECRET_KEY - Endpoint:
https://test-payment.momo.vn/v2/gateway/api
- Environment:
-
ZaloPay (Zalo integrated)
- Environment:
ZALOPAY_APP_ID,ZALOPAY_KEY1,ZALOPAY_KEY2 - Endpoint:
https://sb-openapi.zalopay.vn/v2
- Environment:
Callback Processing
Webhook URL Pattern: /payments/callback/{provider}
Supports both:
- Query parameters (VNPay)
- Request body (MoMo, ZaloPay)
- Merged data handling internally
🔍 SEARCH MODULE
Controllers & Endpoints
File: apps/api/src/modules/search/presentation/controllers/search.controller.ts
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /search |
No | Full-text search (public) |
| GET | /search/geo |
No | Geographic radius search (public) |
| POST | /search/reindex |
JWT+Admin | Reindex all properties (admin) |
DTOs
SearchPropertiesDto (Full-text search)
{
q?: string, // Free-text query, e.g., 'chung cu quan 7'
propertyType?: string, // Filter by type
transactionType?: string, // 'sale' or 'rent'
priceMin?: number, // Min price in VND
priceMax?: number, // Max price in VND
areaMin?: number, // Min area in m²
areaMax?: number, // Max area in m²
bedrooms?: number, // Number of bedrooms
district?: string, // District name
city?: string, // City name
sortBy?: 'price_asc' | 'price_desc' | 'date_desc' | 'relevance',
page?: number, // 1-based, default: 1
perPage?: number, // Default: 20, Max: 100
}
GeoSearchDto (Geographic search)
{
lat: number, // Latitude, -90 to 90
lng: number, // Longitude, -180 to 180
radiusKm: number, // Radius, 0.1 to 100
propertyType?: string,
transactionType?: string,
priceMin?: number,
priceMax?: number,
sortBy?: 'distance' | 'price_asc' | 'price_desc' | 'date_desc',
page?: number, // Default: 1
perPage?: number, // Default: 20, Max: 100
}
Search Engine
Typesense integration for fast full-text & faceted search
- Environment:
TYPESENSE_HOST,TYPESENSE_PORT,TYPESENSE_API_KEY - Default:
http://localhost:8108
Response Structure
SearchResult
{
results: SearchHit[],
facets?: {
propertyType?: { value: string; count: number }[],
district?: { value: string; count: number }[],
transactionType?: { value: string; count: number }[],
},
total: number,
page: number,
perPage: number,
totalPages: number,
}
🗄️ Database & Environment
PostgreSQL with PostGIS
DB_HOST=localhost
DB_PORT=5432
DB_NAME=goodgo
DB_USER=goodgo
DB_PASSWORD=<change_me>
DATABASE_URL=postgresql://goodgo:password@localhost:5432/goodgo?schema=public
Redis Cache
REDIS_URL=redis://localhost:6379
Key Environment Variables
# JWT Secrets (REQUIRED)
JWT_SECRET=<openssl rand -base64 48>
JWT_REFRESH_SECRET=<openssl rand -base64 48>
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
# Node Environment
NODE_ENV=development|test|production
PORT=3001 # API port
# OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
ZALO_APP_ID=
ZALO_APP_SECRET=
# Typesense Search
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_API_KEY=
# MinIO/S3 Storage
MINIO_ENDPOINT=localhost
MINIO_PORT=9000
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=goodgo-media
# Payment Gateways
VNPAY_TMN_CODE=
VNPAY_HASH_SECRET=
MOMO_PARTNER_CODE=
ZALOPAY_APP_ID=
# Logging
LOG_LEVEL=info
🧪 Existing Test Setup
Playwright Configuration
File: playwright.config.ts
testDir: './e2e'
globalSetup: './e2e/global-setup.ts'
globalTeardown: './e2e/global-teardown.ts'
Projects:
- "api": Tests NestJS API (port 3001)
baseURL: http://localhost:3001/api/v1
- "web": Tests Next.js frontend (port 3000)
baseURL: http://localhost:3000
Playwright Scripts
pnpm test:e2e # Run all E2E tests
pnpm test:e2e:api # API tests only
pnpm test:e2e:web # Web tests only
pnpm test:e2e:report # Show HTML report
Test Database
- CI uses
goodgo_testdatabase - Local uses
.env.testfor test database URL - Migrations & seed run in
global-setup.ts - Cleanup in
global-teardown.ts
Example E2E Test
File: e2e/api/auth-register.spec.ts
import { test, expect } from '@playwright/test';
import { createTestUser } from '../fixtures';
test.describe('POST /auth/register', () => {
test('registers a new user and returns token pair', async ({ request }) => {
const user = createTestUser();
const res = await request.post('/auth/register', { data: user });
expect(res.status()).toBe(201);
const body = await res.json();
expect(body).toHaveProperty('accessToken');
expect(body).toHaveProperty('refreshToken');
});
});
Unit Tests (Vitest)
pnpm test # Run unit tests (API only)
pnpm test:integration # Integration tests
🔄 CI/CD Setup
GitHub Actions Workflows
ci.yml - Lint → Typecheck → Test → Build
- Runs on:
push mainandpull_request - Services: PostgreSQL 16 + PostGIS
- Steps: lint → typecheck → test → build
e2e.yml - Playwright E2E Tests
- Runs on:
push mainandpull_request - Services:
- PostgreSQL 16 + PostGIS
- Redis 7
- Typesense 27.1
- MinIO (S3-compatible storage)
- Artifacts: HTML report + traces
security.yml - Code Security
- Dependency scanning
- SAST analysis
deploy.yml - Production Deployment
- Docker builds
- Registry push
- Deployment orchestration
📊 Architecture Patterns
NestJS CQRS Pattern
Each module uses:
-
Commands (Write operations)
CommandBus.execute(command)- Located in
application/commands/ - Handlers in
application/commands/{command}/
-
Queries (Read operations)
QueryBus.execute(query)- Located in
application/queries/ - Handlers in
application/queries/{query}/
Example:
// In controller
const result = await this.commandBus.execute(
new CreateListingCommand(userId, ...)
);
const profile = await this.queryBus.execute(
new GetProfileQuery(userId)
);
Guards & Interceptors
JwtAuthGuard- Validates JWT tokenLocalAuthGuard- Email/password validationRolesGuard- Role-based access controlQuotaGuard- Subscription quota enforcementFileValidationPipe- File upload validation
🚀 Starting the API
Local Development
# Install dependencies
pnpm install
# Generate Prisma client
pnpm db:generate
# Run migrations
pnpm db:migrate:dev
# Seed data (users, listings, etc.)
pnpm db:seed
# Start API (and Web)
pnpm dev
# API will be available at:
# http://localhost:3001/api/v1
# Swagger UI: http://localhost:3001/api/v1/docs
With Docker
docker-compose up
# Services: PostgreSQL, Redis, Typesense, MinIO, API, Web
🎯 K6 Load Testing Recommendations
Key Endpoints to Test
-
Authentication (High priority)
- Register:
POST /auth/register - Login:
POST /auth/login - Refresh:
POST /auth/refresh - Profile:
GET /auth/profile(authenticated)
- Register:
-
Listings (High priority)
- Create:
POST /listings(quota-gated) - Search:
GET /listings(public, high volume) - Detail:
GET /listings/:id(public, high volume)
- Create:
-
Search (High priority)
- Full-text:
GET /search?q=...(public, high volume) - Geo:
GET /search/geo?lat=...&lng=...(public, high volume)
- Full-text:
-
Payments (Medium priority)
- Create:
POST /payments(authenticated) - List:
GET /payments(authenticated) - Webhook:
POST /payments/callback/:provider(unthrottled)
- Create:
-
Admin Endpoints (Medium priority, restricted)
- Moderate listings:
PATCH /listings/:id/moderate - List pending:
GET /listings/pending - Verify KYC:
PATCH /auth/kyc - Reindex:
POST /search/reindex
- Moderate listings:
K6 Script Structure
import http from 'k6/http';
import { check, group, sleep } from 'k6';
const BASE_URL = 'http://localhost:3001/api/v1';
// Stage-based load: ramp up → sustained → ramp down
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Sustained
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.1'],
},
};
export default function() {
// Test scenarios here
}
Data Generation Tips
- Use test fixture users from Playwright tests
- Leverage Prisma seed data (districts, property types)
- Generate realistic search queries
- Test with various geo coordinates (Ho Chi Minh City: ~10.77°N, 106.70°E)
📁 File Locations Quick Reference
apps/api/
├── src/
│ ├── main.ts # API entry point (port 3001)
│ ├── app.module.ts # Root module
│ └── modules/
│ ├── auth/
│ │ ├── presentation/controllers/auth.controller.ts
│ │ ├── presentation/dto/
│ │ │ ├── login.dto.ts
│ │ │ ├── register.dto.ts
│ │ │ ├── refresh-token.dto.ts
│ │ │ └── verify-kyc.dto.ts
│ │ ├── application/commands/
│ │ ├── application/queries/
│ │ ├── infrastructure/services/token.service.ts
│ │ └── domain/
│ ├── listings/
│ │ ├── presentation/controllers/listings.controller.ts
│ │ ├── presentation/dto/
│ │ │ ├── create-listing.dto.ts
│ │ │ ├── search-listings.dto.ts
│ │ │ ├── update-listing-status.dto.ts
│ │ │ └── moderate-listing.dto.ts
│ │ └── ...
│ ├── payments/
│ │ ├── presentation/controllers/payments.controller.ts
│ │ ├── presentation/dto/
│ │ │ ├── create-payment.dto.ts
│ │ │ ├── list-transactions.dto.ts
│ │ │ └── refund-payment.dto.ts
│ │ └── ...
│ ├── search/
│ │ ├── presentation/controllers/search.controller.ts
│ │ ├── presentation/dto/
│ │ │ ├── search-properties.dto.ts
│ │ │ └── geo-search.dto.ts
│ │ └── ...
│ └── ...
│
└── package.json # Dependencies, scripts
e2e/
├── api/ # Playwright API tests
│ ├── auth-register.spec.ts
│ ├── auth-refresh.spec.ts
│ └── ...
├── web/ # Playwright web tests
├── fixtures.ts # Test data generators
├── global-setup.ts # DB setup before tests
└── global-teardown.ts # DB cleanup after tests
playwright.config.ts # Playwright config
.github/workflows/
├── ci.yml # Lint → typecheck → test → build
├── e2e.yml # Playwright E2E
├── security.yml # Security scanning
└── deploy.yml # Production deployment
.env.example # Environment variable template
.env.test # Test database connection
🔗 Useful Links & References
- API Swagger Docs:
http://localhost:3001/api/v1/docs - Project Root Docs:
CLAUDE.md - Existing Analysis:
CODEBASE_ANALYSIS.md,EXPLORATION_REPORT.md - Frontend Docs:
FRONTEND_EXPLORATION.md
✅ Summary for K6 Implementation
No existing K6 setup — you have a clean slate!
Key endpoints identified across:
- Auth (register, login, refresh, profile)
- Listings (create, search, detail, moderate)
- Search (full-text, geo)
- Payments (create, callback, list, refund)
- Admin (moderate, KYC, reindex)
Rate limits to consider:
- Auth: 5/hour per endpoint
- Payments callback: 20/min
- Others: No limit (except quota guards on create operations)
Infrastructure ready:
- Turbo monorepo for dependency management
- PostgreSQL + PostGIS for spatial data
- Typesense for search indexing
- Redis for caching
- MinIO for media storage
- Prometheus metrics endpoint
Tests can be integrated into CI/CD pipeline via .github/workflows/ (suggested: new load-test.yml)