Instruction file imported from javier1234559/weebuns-server-ai (
.cursor/rules/main.mdc). Copyright stays with the author.
Project Brief
Core Requirements
- Language learning platform API server
- Lesson management system with 4 skills (Reading, Writing, Listening, Speaking)
- User management with student and teacher profiles
- Token-based economy for premium features
- AI integration for learning assistance
Project Goals
- Build a robust API server for language learning
- Enable flexible lesson creation and management
- Support multiple learning modes (Practice/Test)
- Integrate AI for enhanced learning experience
- Implement token-based premium features
Technical Foundation
- NestJS with TypeScript
- Prisma ORM + PostgreSQL
- Swagger API documentation
- Modular architecture
Directory Structure
models/
└── feature-name/
├── dto/
│ ├── feature-request.dto.ts
│ └── feature-response.dto.ts
├── entities/
│ └── feature.entity.ts
├── interface/
│ ├── feature.interface.ts
│ └── feature-service.interface.ts
├── types/
│ └── feature.types.ts
├── feature.controller.ts
├── feature.module.ts
└── feature.service.ts
Development Flow
-
Interface First
- Define entity interface in
interface/feature.interface.ts - Create service interface in
interface/feature-service.interface.ts - Define all required methods and their signatures
- Define entity interface in
-
DTO Structure
- Create request DTOs with validation decorators
- Define response DTOs for data transformation
- Just create in 2 file dto request and response which mean , place every request in feature-request.dto.ts and the same with feature-response.dto just for simple
- Use
@ApiProperty()decorators for Swagger documentation - Group related DTOs by feature name (e.g.,
lesson-request.dto.ts)
-
Service Implementation
- Implement service interface
- Use dependency injection for required services
- Handle data transformation and business logic
- Implement error handling and validation
-
Controller Setup
- Define routes with appropriate HTTP methods
- Use role-based access control decorators
- Implement Swagger documentation
- Handle request validation and response transformation
Code Standards
-
Interface Patterns
import { User as PrismaUser, StudentProfile, TeacherProfile, } from '@prisma/client'; export interface IUser extends PrismaUser {} export interface IFeatureService { create(data: CreateFeatureDto): Promise<CreateFeatureResponse>; findById(id: string): Promise<FeatureResponse>; update(data: UpdateFeatureDto) : Promise<UpdateFeatureResponse> getAll(query : FindAllFeatureQuery) : Promise<FeaturesResponse> delete(id: string) : Promise<DeleteFeatureResponse> // ... other methods } -
DTO Patterns
export class FindAllUserQuery extends PaginationInputDto { //docs @ApiPropertyOptional() @IsOptional() @IsString() search?: string; } export class UserResponse { @ApiProperty() user: Omit<User, 'passwordHash'>; } export class UsersResponse { @ApiProperty() data: Omit<User, 'passwordHash'>[]; @ApiProperty() pagination: PaginationOutputDto; } export class DeleteUserResponse { @ApiProperty() message: string; } export class CreateUserResponse { @ApiProperty() user: Omit<User, 'passwordHash'>; } -
Controller Patterns
@Controller('users') @ApiTags('users') @ApiBearerAuth() @UseGuards(AuthGuard, RolesGuard) export class UserController { constructor(private readonly userService: UserService) {} @Get() @Roles(UserRole.ADMIN) @ApiQuery({ type: FindAllUserQuery }) @ApiResponse({ status: HttpStatus.OK, type: UsersResponse, }) findAll(@Query() dto: FindAllUserQuery): Promise<UsersResponse> { return this.userService.findAll(dto); } @Get(':id') @ApiResponse({ status: HttpStatus.OK, type: UserResponse, }) findById(@Param('id') id: string): Promise<UserResponse> { return this.userService.findById(id); } @Post('teachers') @Roles(UserRole.ADMIN) @ApiResponse({ status: HttpStatus.CREATED, type: UserResponse, }) createTeacher(@Body() teacherDto: TeacherDto): Promise<UserResponse> { return this.userService.createTeacher(teacherDto); } @Patch('teachers/:id') @Roles(UserRole.ADMIN) @ApiResponse({ status: HttpStatus.OK, type: UserResponse, }) updateTeacher( @Param('id') id: string, @Body() teacherDto: TeacherDto, ): Promise<UserResponse> { return this.userService.updateTeacher(id, teacherDto); } @Patch('teachers/:id/profile') @Roles(UserRole.TEACHER) @ApiResponse({ status: HttpStatus.OK, type: UserResponse, }) updateTeacherProfile( @Param('id') id: string, @Body() profileDto: ProfileDto, ): Promise<UserResponse> { return this.userService.updateProfileTeacher(id, profileDto); } @Patch('students/:id/profile') @Roles(UserRole.USER) @ApiResponse({ status: HttpStatus.OK, type: UserResponse, }) updateStudentProfile( @Param('id') id: string, @Body() profileDto: ProfileDto, ): Promise<UserResponse> { return this.userService.updateProfileStudent(id, profileDto); } @Delete(':id') @Roles(UserRole.ADMIN) @ApiResponse({ status: HttpStatus.OK, type: DeleteUserResponse, }) remove(@Param('id') id: string): Promise<DeleteUserResponse> { return this.userService.remove(id); } } -
Service Patterns
@Injectable()
export class UserService implements UserServiceInterface {
constructor(private readonly prisma: PrismaService) {}
private readonly userIncludeQuery = {
include: {
teacherProfile: true,
studentProfile: true,
},
};
private transformUserResponse(user: any): Omit<User, 'passwordHash'> {
const { passwordHash, ...userWithoutPassword } = user;
return userWithoutPassword;
}
async findAll(findAllUsersDto: FindAllUserQuery): Promise<UsersResponse> {
const { page, perPage, search } = findAllUsersDto;
const queryOptions = {
where: {
...notDeletedQuery,
...(search
? searchQuery(search, ['username', 'email', 'firstName', 'lastName'])
: {}),
},
orderBy: { createdAt: 'desc' },
...paginationQuery(page, perPage),
...this.userIncludeQuery,
};
const [users, totalItems] = await Promise.all([
this.prisma.user.findMany(queryOptions),
this.prisma.user.count({ where: queryOptions.where }),
]);
return {
data: users.map((user) => this.transformUserResponse(user)),
pagination: calculatePagination(totalItems, findAllUsersDto),
};
}
async findById(id: string): Promise<UserResponse> {
const user = await this.prisma.user.findUnique({
where: { id },
...this.userIncludeQuery,
});
return { user: this.transformUserResponse(user) };
}
async createTeacher(teacherDto: TeacherDto): Promise<UserResponse> {
const {
password,
specialization,
qualification,
teachingExperience,
hourlyRate,
...userData
} = teacherDto;
const existingUser = await this.prisma.user.findFirst({
where: {
OR: [{ email: userData.email }, { username: userData.username }],
...notDeletedQuery,
},
});
if (existingUser) {
throw new ConflictException('Email or username already exists');
}
const hashedPassword = await bcrypt.hash(password, 10);
const newTeacher = await this.prisma.user.create({
data: {
...userData,
passwordHash: hashedPassword,
role: UserRole.teacher,
authProvider: AuthProvider.local,
teacherProfile: {
create: {
specialization,
qualification,
teachingExperience,
hourlyRate,
},
},
},
...this.userIncludeQuery,
});
return { user: this.transformUserResponse(newTeacher) };
}
async updateTeacher(
id: string,
teacherDto: TeacherDto,
): Promise<UserResponse> {
const existingTeacher = await this.prisma.user.findFirst({
where: {
id,
role: UserRole.teacher,
...notDeletedQuery,
},
...this.userIncludeQuery,
});
if (!existingTeacher) {
throw new NotFoundException(`Teacher with ID ${id} not found`);
}
const {
password,
specialization,
qualification,
teachingExperience,
hourlyRate,
...userData
} = teacherDto;
const updatedTeacher = await this.prisma.user.update({
where: { id },
data: {
...userData,
teacherProfile: {
update: {
specialization,
qualification,
teachingExperience,
hourlyRate,
},
},
},
...this.userIncludeQuery,
});
return { user: this.transformUserResponse(updatedTeacher) };
}
async updateProfileTeacher(
userId: string,
data: ProfileDto,
): Promise<UserResponse> {
const user = await this.prisma.user.findFirst({
where: {
id: userId,
role: UserRole.teacher,
...notDeletedQuery,
},
...this.userIncludeQuery,
});
if (!user) {
throw new NotFoundException('Teacher not found');
}
const {
specialization,
qualification,
teachingExperience,
hourlyRate,
...basicData
} = data;
const updatedUser = await this.prisma.user.update({
where: { id: userId },
data: {
...basicData,
teacherProfile: user.teacherProfile
? {
update: {
specialization,
qualification,
teachingExperience,
hourlyRate,
},
}
: {
create: {
specialization,
qualification,
teachingExperience,
hourlyRate,
},
},
},
...this.userIncludeQuery,
});
return { user: this.transformUserResponse(updatedUser) };
}
async updateProfileStudent(
userId: string,
data: ProfileDto,
): Promise<UserResponse> {
const user = await this.prisma.user.findFirst({
where: {
id: userId,
role: UserRole.user,
...notDeletedQuery,
},
...this.userIncludeQuery,
});
if (!user) {
throw new NotFoundException('Student not found');
}
const {
targetStudyDuration,
targetReading,
targetListening,
targetWriting,
targetSpeaking,
nextExamDate,
...basicData
} = data;
const updatedUser = await this.prisma.user.update({
where: { id: userId },
data: {
...basicData,
studentProfile: user.studentProfile
? {
update: {
targetStudyDuration,
targetReading,
targetListening,
targetWriting,
targetSpeaking,
nextExamDate,
},
}
: {
create: {
targetStudyDuration,
targetReading,
targetListening,
targetWriting,
targetSpeaking,
nextExamDate,
tokensBalance: 0,
},
},
},
...this.userIncludeQuery,
});
return { user: this.transformUserResponse(updatedUser) };
}
async remove(id: string): Promise<DeleteUserResponse> {
const user = await this.prisma.user.findFirst({
where: { id, ...notDeletedQuery },
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
await this.prisma.user.update({
where: { id },
data: {
...softDeleteQuery,
},
});
return { message: 'User deleted successfully' };
}
async updateProfile(
id: string,
updateUserInput: UpdateProfileUserDto,
): Promise<UserResponse> {
const user = await this.prisma.user.findFirst({
where: { id, ...notDeletedQuery },
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
const updatedUser = await this.prisma.user.update({
where: { id },
data: {
firstName: updateUserInput.firstName,
lastName: updateUserInput.lastName,
email: updateUserInput.email,
username: updateUserInput.username,
profilePicture: updateUserInput.profilePicture,
},
...this.userIncludeQuery,
});
const { passwordHash, ...userWithoutPassword } = updatedUser;
return { user: userWithoutPassword };
}
}
Role-Based Access
- Define clear role requirements for each endpoint
- Use
@Roles()decorator for access control - Common roles: ADMIN, TEACHER, STUDENT
- Document role requirements in Swagger
Swagger Documentation
- Use
@ApiTags()for feature grouping - Document all DTOs with
@ApiProperty() - Provide response examples
- Include role requirements in documentation
Error Handling
- Use custom exceptions when needed
- Transform errors to consistent response format
- Validate DTOs using class-validator
- Handle edge cases explicitly