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

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();
}),
);
}
}