feat: Initialize NestJS backend project with Prisma, core modules, configuration, and common utilities.
This commit is contained in:
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal 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
23
src/app.controller.ts
Normal 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
54
src/app.module.ts
Normal 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
38
src/app.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/common/filters/http-exception.filter.ts
Normal file
58
src/common/filters/http-exception.filter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1
src/common/filters/index.ts
Normal file
1
src/common/filters/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './http-exception.filter';
|
||||
1
src/common/interceptors/index.ts
Normal file
1
src/common/interceptors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './transform.interceptor';
|
||||
32
src/common/interceptors/transform.interceptor.ts
Normal file
32
src/common/interceptors/transform.interceptor.ts
Normal 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
100
src/config/configuration.ts
Normal 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
1
src/config/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as configuration } from './configuration';
|
||||
91
src/main.ts
Normal file
91
src/main.ts
Normal 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
2
src/prisma/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './prisma.module';
|
||||
export * from './prisma.service';
|
||||
9
src/prisma/prisma.module.ts
Normal file
9
src/prisma/prisma.module.ts
Normal 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 {}
|
||||
45
src/prisma/prisma.service.ts
Normal file
45
src/prisma/prisma.service.ts
Normal 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();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user