feat: Initialize NestJS backend project with Prisma, core modules, configuration, and common utilities.

This commit is contained in:
pradeepkumar
2025-12-18 23:05:04 +05:30
commit 7990846711
28 changed files with 21284 additions and 0 deletions

72
.env.example Normal file
View File

@@ -0,0 +1,72 @@
# ===========================================
# Real Estate Agent Platform - Backend Environment Variables
# Copy this file to .env and fill in your values
# ===========================================
# Application
NODE_ENV=development
PORT=3001
APP_NAME="Real Estate Agent Platform"
API_URL=http://localhost:3001
FRONTEND_URL=http://localhost:3000
ADMIN_URL=http://localhost:3002
# Database (PostgreSQL)
DATABASE_URL="postgresql://postgres:password@localhost:5432/real_estate_db?schema=public"
# JWT Authentication
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_ACCESS_EXPIRATION=15m
JWT_REFRESH_EXPIRATION=7d
# Password Hashing
BCRYPT_SALT_ROUNDS=12
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_CALLBACK_URL=http://localhost:3001/auth/google/callback
# Facebook OAuth
FACEBOOK_APP_ID=your-facebook-app-id
FACEBOOK_APP_SECRET=your-facebook-app-secret
FACEBOOK_CALLBACK_URL=http://localhost:3001/auth/facebook/callback
# AWS S3 (File Storage)
AWS_ACCESS_KEY_ID=your-aws-access-key
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
AWS_REGION=us-east-1
AWS_S3_BUCKET=your-s3-bucket-name
# Email (SMTP / SendGrid)
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USER=apikey
MAIL_PASSWORD=your-sendgrid-api-key
MAIL_FROM="Real Estate Platform <noreply@yourdomain.com>"
# Stripe (Payments)
STRIPE_SECRET_KEY=sk_test_your-stripe-secret-key
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret
STRIPE_PUBLISHABLE_KEY=pk_test_your-stripe-publishable-key
# Firebase (Push Notifications)
FIREBASE_PROJECT_ID=your-firebase-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYour-Private-Key\n-----END PRIVATE KEY-----"
# Redis (Caching & Queue)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Rate Limiting
THROTTLE_TTL=60
THROTTLE_LIMIT=100
# Logging
LOG_LEVEL=debug
# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:3002

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Dependencies
node_modules/
# Build
dist/
build/
# Environment variables
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Testing
coverage/
.nyc_output/
# Prisma
/generated/prisma
# Temporary files
tmp/
temp/
# Debug
*.pid
*.seed
*.pid.lock

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

98
README.md Normal file
View File

@@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

35
eslint.config.mjs Normal file
View File

@@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);

8
nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

20178
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

143
package.json Normal file
View File

@@ -0,0 +1,143 @@
{
"name": "real-estate-backend",
"version": "1.0.0",
"description": "Real Estate Agent Platform - Backend API",
"author": "SuperLabs",
"private": true,
"license": "MIT",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"db:generate": "npx prisma generate",
"db:migrate": "npx prisma migrate dev",
"db:migrate:prod": "npx prisma migrate deploy",
"db:push": "npx prisma db push",
"db:seed": "npx prisma db seed",
"db:studio": "npx prisma studio",
"db:reset": "npx prisma migrate reset"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.954.0",
"@aws-sdk/lib-storage": "^3.954.0",
"@aws-sdk/s3-request-presigner": "^3.954.0",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/axios": "^4.0.1",
"@nestjs/bull": "^11.0.4",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/cqrs": "^11.0.3",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-socket.io": "^11.1.9",
"@nestjs/schedule": "^6.1.0",
"@nestjs/swagger": "^11.2.3",
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.1.9",
"@prisma/client": "^7.2.0",
"@sendgrid/mail": "^8.1.6",
"argon2": "^0.44.0",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"bull": "^4.16.5",
"bullmq": "^5.66.1",
"cache-manager": "^7.2.7",
"cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"cookie-parser": "^1.4.7",
"cross-env": "^10.1.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.19",
"decimal.js": "^10.6.0",
"firebase-admin": "^13.6.0",
"google-auth-library": "^10.5.0",
"helmet": "^8.1.0",
"ioredis": "^5.8.2",
"joi": "^18.0.2",
"mime-types": "^3.0.2",
"multer": "^2.0.2",
"nest-winston": "^1.10.2",
"nestjs-pino": "^4.5.0",
"nodemailer": "^7.0.11",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"pg": "^8.16.3",
"pino-http": "^11.0.0",
"prisma": "^7.2.0",
"redis": "^5.10.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sharp": "^0.34.5",
"slugify": "^1.6.6",
"socket.io": "^4.8.1",
"stripe": "^20.1.0",
"uuid": "^13.0.0",
"winston": "^3.19.0",
"zod": "^4.2.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@faker-js/faker": "^10.1.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/multer": "^2.0.0",
"@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.4",
"@types/passport-google-oauth20": "^2.0.17",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"autocannon": "^8.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

14
prisma.config.ts Normal file
View File

@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

147
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,147 @@
// Real Estate Agent Platform - Database Schema
// Milestone 1: Foundation & Authentication
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ===========================================
// ENUMS
// ===========================================
enum UserRole {
USER
AGENT
ADMIN
}
enum UserStatus {
ACTIVE
INACTIVE
SUSPENDED
PENDING_VERIFICATION
}
enum AuthProvider {
LOCAL
GOOGLE
FACEBOOK
}
enum VerificationStatus {
PENDING
APPROVED
REJECTED
}
// ===========================================
// USER & AUTHENTICATION (Milestone 1)
// ===========================================
model User {
id String @id @default(uuid())
email String @unique
password String? // Null for social login users
firstName String?
lastName String?
phone String?
avatar String?
role UserRole @default(USER)
status UserStatus @default(ACTIVE)
emailVerified Boolean @default(false)
emailVerifiedAt DateTime?
// Social Auth
authProvider AuthProvider @default(LOCAL)
googleId String? @unique
facebookId String? @unique
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
// Relations (will be added in later milestones)
agentProfile AgentProfile?
sessions Session[]
passwordResets PasswordReset[]
@@index([email])
@@index([role])
@@index([status])
@@map("users")
}
model Session {
id String @id @default(uuid())
userId String
token String @unique
refreshToken String? @unique
userAgent String?
ipAddress String?
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([token])
@@map("sessions")
}
model PasswordReset {
id String @id @default(uuid())
userId String
token String @unique
expiresAt DateTime
usedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([token])
@@map("password_resets")
}
// ===========================================
// AGENT PROFILE (Milestone 2 - Placeholder)
// ===========================================
model AgentProfile {
id String @id @default(uuid())
userId String @unique
slug String @unique
bio String?
headline String?
location String?
city String?
state String?
country String?
yearsOfExperience Int?
licenseNumber String?
// Verification
verificationStatus VerificationStatus @default(PENDING)
verifiedAt DateTime?
// Profile completeness
isProfileComplete Boolean @default(false)
profileCompleteness Int @default(0)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([slug])
@@index([verificationStatus])
@@index([city, state])
@@map("agent_profiles")
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

23
src/app.controller.ts Normal file
View File

@@ -0,0 +1,23 @@
import { Controller, Get } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AppService } from './app.service';
@ApiTags('Health')
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@ApiOperation({ summary: 'Get API welcome message' })
@ApiResponse({ status: 200, description: 'API is running' })
getHello(): string {
return this.appService.getHello();
}
@Get('health')
@ApiOperation({ summary: 'Health check endpoint' })
@ApiResponse({ status: 200, description: 'Service is healthy' })
getHealth() {
return this.appService.getHealth();
}
}

54
src/app.module.ts Normal file
View File

@@ -0,0 +1,54 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { configuration } from './config';
import { PrismaModule } from './prisma';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
// Configuration
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
envFilePath: ['.env.local', '.env'],
}),
// Rate Limiting
ThrottlerModule.forRoot([
{
ttl: 60000, // 60 seconds
limit: 100, // 100 requests per minute
},
]),
// Task Scheduling
ScheduleModule.forRoot(),
// Event Emitter
EventEmitterModule.forRoot(),
// Database
PrismaModule,
// Feature Modules (will be added as we develop them)
// AuthModule,
// UsersModule,
// AgentsModule,
],
controllers: [AppController],
providers: [
AppService,
// Global Rate Limiting Guard
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}

38
src/app.service.ts Normal file
View File

@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from './prisma';
@Injectable()
export class AppService {
constructor(
private readonly configService: ConfigService,
private readonly prisma: PrismaService,
) {}
getHello(): string {
return 'Real Estate Agent Platform API v1.0';
}
async getHealth() {
const dbHealthy = await this.checkDatabaseHealth();
return {
status: 'ok',
timestamp: new Date().toISOString(),
environment: this.configService.get<string>('app.env'),
version: '1.0.0',
services: {
database: dbHealthy ? 'healthy' : 'unhealthy',
},
};
}
private async checkDatabaseHealth(): Promise<boolean> {
try {
await this.prisma.$queryRaw`SELECT 1`;
return true;
} catch {
return false;
}
}
}

View File

@@ -0,0 +1,58 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status: number;
let message: string | object;
let error: string;
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'object') {
message = (exceptionResponse as any).message || exception.message;
error = (exceptionResponse as any).error || 'Error';
} else {
message = exceptionResponse;
error = 'Error';
}
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = 'Internal server error';
error = 'Internal Server Error';
// Log unexpected errors
this.logger.error(
`Unexpected error: ${exception}`,
exception instanceof Error ? exception.stack : undefined,
);
}
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
error,
message,
};
response.status(status).json(errorResponse);
}
}

View File

@@ -0,0 +1 @@
export * from './http-exception.filter';

View File

@@ -0,0 +1 @@
export * from './transform.interceptor';

View File

@@ -0,0 +1,32 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
success: boolean;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}

100
src/config/configuration.ts Normal file
View File

@@ -0,0 +1,100 @@
export default () => ({
// Application
app: {
name: process.env.APP_NAME || 'Real Estate Agent Platform',
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '3001', 10),
apiUrl: process.env.API_URL || 'http://localhost:3001',
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000',
adminUrl: process.env.ADMIN_URL || 'http://localhost:3002',
},
// Database
database: {
url: process.env.DATABASE_URL,
},
// JWT Authentication
jwt: {
secret: process.env.JWT_SECRET || 'super-secret-key',
accessExpiration: process.env.JWT_ACCESS_EXPIRATION || '15m',
refreshExpiration: process.env.JWT_REFRESH_EXPIRATION || '7d',
},
// Password Hashing
bcrypt: {
saltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS || '12', 10),
},
// Google OAuth
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackUrl: process.env.GOOGLE_CALLBACK_URL,
},
// Facebook OAuth
facebook: {
appId: process.env.FACEBOOK_APP_ID,
appSecret: process.env.FACEBOOK_APP_SECRET,
callbackUrl: process.env.FACEBOOK_CALLBACK_URL,
},
// AWS S3
aws: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION || 'us-east-1',
s3Bucket: process.env.AWS_S3_BUCKET,
},
// Email
mail: {
host: process.env.MAIL_HOST,
port: parseInt(process.env.MAIL_PORT || '587', 10),
user: process.env.MAIL_USER,
password: process.env.MAIL_PASSWORD,
from: process.env.MAIL_FROM,
},
// Stripe
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
},
// Firebase
firebase: {
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
},
// Redis
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD || undefined,
db: parseInt(process.env.REDIS_DB || '0', 10),
},
// Rate Limiting
throttle: {
ttl: parseInt(process.env.THROTTLE_TTL || '60', 10),
limit: parseInt(process.env.THROTTLE_LIMIT || '100', 10),
},
// Logging
logging: {
level: process.env.LOG_LEVEL || 'debug',
},
// CORS
cors: {
origins: process.env.CORS_ORIGINS?.split(',') || [
'http://localhost:3000',
'http://localhost:3002',
],
},
});

1
src/config/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as configuration } from './configuration';

91
src/main.ts Normal file
View File

@@ -0,0 +1,91 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import helmet from 'helmet';
import * as cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters';
import { TransformInterceptor } from './common/interceptors';
async function bootstrap() {
const logger = new Logger('Bootstrap');
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const port = configService.get<number>('app.port') || 3001;
const appEnv = configService.get<string>('app.env') || 'development';
const corsOrigins = configService.get<string[]>('cors.origins') || [];
// Security
app.use(helmet());
app.use(cookieParser());
// CORS
app.enableCors({
origin: corsOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
});
// Global prefix
app.setGlobalPrefix('api/v1');
// Global pipes
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// Global filters
app.useGlobalFilters(new AllExceptionsFilter());
// Global interceptors
app.useGlobalInterceptors(new TransformInterceptor());
// Swagger documentation (only in development)
if (appEnv !== 'production') {
const config = new DocumentBuilder()
.setTitle('Real Estate Agent Platform API')
.setDescription('API documentation for the Real Estate Agent Platform')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'JWT-auth',
)
.addTag('Auth', 'Authentication endpoints')
.addTag('Users', 'User management endpoints')
.addTag('Agents', 'Agent management endpoints')
.addTag('Health', 'Health check endpoints')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document, {
swaggerOptions: {
persistAuthorization: true,
},
});
logger.log(`Swagger documentation available at http://localhost:${port}/api/docs`);
}
await app.listen(port);
logger.log(`Application is running on: http://localhost:${port}`);
logger.log(`Environment: ${appEnv}`);
}
bootstrap();

2
src/prisma/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './prisma.module';
export * from './prisma.service';

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,45 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
super({
log:
process.env.NODE_ENV === 'development'
? ['query', 'info', 'warn', 'error']
: ['error'],
});
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
async cleanDatabase() {
if (process.env.NODE_ENV === 'production') {
throw new Error('cleanDatabase is not allowed in production');
}
const models = Reflect.ownKeys(this).filter(
(key) => typeof key === 'string' && !key.startsWith('_') && !key.startsWith('$'),
);
return Promise.all(
models.map((modelKey) => {
const model = this[modelKey as string];
if (model && typeof model.deleteMany === 'function') {
return model.deleteMany();
}
return Promise.resolve();
}),
);
}
}

25
test/app.e2e-spec.ts Normal file
View File

@@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file
View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}