Skip to main content

Overview

TrackGeek’s review system allows users to provide detailed, structured feedback on media with category-specific ratings tailored to each media type. Each review includes an overall rating, optional category ratings, written feedback, and recommendation status.

Review Architecture

Common Features

All review models share these characteristics:
  • Unique Constraint: One review per user per media item
  • Overall Rating: Required decimal rating (0-10)
  • Category Ratings: Optional decimal ratings (0-10) specific to media type
  • Written Content: Summary, notes, pros/cons, recommendations
  • Timestamps: Automatic createdAt and updatedAt

Rating Scale

All ratings use a 0-10 decimal scale for precision:
@IsDecimal()
@Max(10)
@Min(0)
readonly overall: number;
Source: src/modules/anime-review/dtos/create-anime-review.dto.ts:4-7
Decimal ratings allow for granular scores like 7.5, 8.3, etc., providing more nuanced reviews than integer scales.

Review Types by Media

AnimeReview Model

Anime reviews focus on animation-specific quality metrics.
model AnimeReview {
  id          String   @id @default(uuid())
  overall     Decimal
  story       Decimal?
  characters  Decimal?
  animation   Decimal?
  sound       Decimal?
  enjoyment   Decimal?
  summary     String?
  notes       String?
  pros        String?
  cons        String?
  recommended Boolean?
  userId      String
  animeId     String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  user  User  @relation(fields: [userId], references: [id])
  anime Anime @relation(fields: [animeId], references: [id])

  @@unique([userId, animeId])
}
Source: prisma/schema.prisma:734-756

Rating Categories

Evaluates narrative quality, plot development, pacing, and storytelling.Consider:
  • Plot coherence and originality
  • Character arcs and development
  • Pacing and narrative structure
  • Theme execution
Evaluates character design, development, and relationships.Consider:
  • Character depth and growth
  • Personality distinctiveness
  • Relationship dynamics
  • Voice acting performance
Evaluates visual quality, art style, and animation fluidity.Consider:
  • Art style and consistency
  • Animation fluidity
  • Action scene choreography
  • Visual effects and cinematography
Evaluates audio quality, music, sound effects, and voice acting.Consider:
  • Opening/ending themes
  • Background music (OST)
  • Sound effect quality
  • Voice acting performance
Subjective measure of personal enjoyment regardless of technical quality.Consider:
  • Emotional impact
  • Rewatchability
  • Personal connection
  • Entertainment value

Written Content

FieldMax LengthDescription
summary250 charsBrief review summary
pros500 charsPositive aspects
cons500 charsNegative aspects
notes1000 charsAdditional thoughts
DTO Validation (src/modules/anime-review/dtos/create-anime-review.dto.ts:39-53):
@IsOptional()
@MaxLength(250)
readonly summary?: string;

@IsOptional()
@MaxLength(500)
readonly pros?: string;

@IsOptional()
@MaxLength(500)
readonly cons?: string;

@IsOptional()
@MaxLength(1000)
readonly notes?: string;

Recommendation System

All review types include an optional recommended boolean field:
recommended Boolean?
This allows users to give a clear yes/no recommendation independent of numerical ratings.
  • Positive but not recommended: High technical quality but niche appeal
  • Negative but recommended: Flawed but unique or important
  • No recommendation: User is neutral or undecided

Review Creation Flow

Review creation follows a consistent pattern across all media types: Example from AnimeReviewService (src/modules/anime-review/anime-review.service.ts:20-60):
async createAnimeReview(createAnimeReviewDto: CreateAnimeReviewDto) {
  const animeReview = await this.databaseService.animeReview.create({
    data: {
      overall: createAnimeReviewDto.overall,
      story: createAnimeReviewDto.story,
      characters: createAnimeReviewDto.characters,
      animation: createAnimeReviewDto.animation,
      sound: createAnimeReviewDto.sound,
      enjoyment: createAnimeReviewDto.enjoyment,
      summary: createAnimeReviewDto.summary,
      pros: createAnimeReviewDto.pros,
      cons: createAnimeReviewDto.cons,
      notes: createAnimeReviewDto.notes,
      recommended: createAnimeReviewDto.recommended,
      animeId: createAnimeReviewDto.animeId,
      userId: createAnimeReviewDto.userId,
    },
    include: {
      anime: true,
      user: {
        select: {
          id: true,
          name: true,
          username: true,
          profile: {
            select: {
              id: true,
              avatarUrl: true,
            },
          },
        },
      },
    },
  });

  await this.queueService.toFeedEventQueue({
    type: FeedEventType.NewReview,
    userId: createAnimeReviewDto.userId,
    metadata: { animeReview },
  });
}

Flow Steps

  1. Validation: DTO validates all rating ranges (0-10) and string lengths
  2. Creation: Insert review with all provided fields
  3. Relations: Include media and user data in response
  4. Feed Event: Queue a NewReview event for follower feeds

Feed Event Integration

Reviews trigger feed events to notify followers: Feed Event Type (prisma/schema.prisma:235):
NewReview
The event metadata contains the full review object including:
  • All ratings (overall + categories)
  • Written content (summary, notes, pros/cons)
  • Recommendation status
  • Related media details
  • User information

Review Queries

The review system supports various query patterns:

Get Reviews by Media

Example (src/modules/anime-review/anime-review.service.ts:90-118):
async getAnimeReviews(getAnimeReviewsDto: GetAnimeReviewsDto) {
  const animeReviews = await this.databaseService.offsetPagination<AnimeReviewFindManyArgs>({
    model: "animeReview",
    itemsPerPage: getAnimeReviewsDto.itemsPerPage,
    page: getAnimeReviewsDto.page,
    where: {
      animeId: getAnimeReviewsDto.animeId,
      userId: getAnimeReviewsDto.userId,
    },
    include: {
      anime: true,
      user: {
        select: {
          id: true,
          name: true,
          username: true,
          profile: {
            select: {
              id: true,
              avatarUrl: true,
            },
          },
        },
      },
    },
  });

  return animeReviews;
}

Get Single Review

Example (src/modules/anime-review/anime-review.service.ts:62-88):
async getAnimeReviewById(animeReviewId: string) {
  const animeReview = await this.databaseService.animeReview.findUnique({
    where: { id: animeReviewId },
    include: {
      anime: true,
      user: {
        select: {
          id: true,
          name: true,
          username: true,
          profile: {
            select: {
              id: true,
              avatarUrl: true,
            },
          },
        },
      },
    },
  });

  if (!animeReview) {
    throw new AppException(ERROR_CODES.REVIEW_NOT_FOUND);
  }

  return animeReview;
}

Review Updates

Users can update their reviews with authorization checks: Example (src/modules/anime-review/anime-review.service.ts:120-149):
async updateAnimeReview(updateAnimeReviewDto: UpdateAnimeReviewDto) {
  const animeReview = await this.databaseService.animeReview.findUnique({
    where: {
      id: updateAnimeReviewDto.animeReviewId,
    },
  });

  if (!animeReview || animeReview.userId !== updateAnimeReviewDto.userId) {
    throw new AppException(ERROR_CODES.REVIEW_NOT_FOUND);
  }

  await this.databaseService.animeReview.update({
    where: {
      id: animeReview.id,
    },
    data: {
      overall: updateAnimeReviewDto.overall,
      story: updateAnimeReviewDto.story,
      characters: updateAnimeReviewDto.characters,
      animation: updateAnimeReviewDto.animation,
      sound: updateAnimeReviewDto.sound,
      enjoyment: updateAnimeReviewDto.enjoyment,
      summary: updateAnimeReviewDto.summary,
      pros: updateAnimeReviewDto.pros,
      cons: updateAnimeReviewDto.cons,
      notes: updateAnimeReviewDto.notes,
      recommended: updateAnimeReviewDto.recommended,
    },
  });
}
Updates verify that the review belongs to the requesting user before allowing modifications.

Review Deletion

Deletion follows the same authorization pattern: Example (src/modules/anime-review/anime-review.service.ts:151-166):
async deleteAnimeReview(deleteAnimeReviewDto: DeleteAnimeReviewDto) {
  const animeReview = await this.databaseService.animeReview.findUnique({
    where: {
      id: deleteAnimeReviewDto.animeReviewId,
    },
  });

  if (!animeReview || animeReview.userId !== deleteAnimeReviewDto.userId) {
    throw new AppException(ERROR_CODES.REVIEW_NOT_FOUND);
  }

  await this.databaseService.animeReview.delete({
    where: { id: animeReview.id },
  });
}

Aggregate Statistics

Reviews enable powerful aggregate statistics:

Average Ratings

Calculate average overall and category ratings per media

Review Count

Total number of reviews per media

Recommendation Rate

Percentage of users who recommend the media

Rating Distribution

Histogram of rating ranges

Top Rated

Media sorted by highest average ratings

Most Reviewed

Media with most review activity

Best Practices

Require Overall

Always require the overall rating; make category ratings optional

Validate Ranges

Enforce 0-10 range validation at DTO level

Character Limits

Set reasonable limits on text fields to encourage concise reviews

User Authorization

Always verify review ownership before updates/deletes

Include Relations

Return media and user data with reviews for context

Feed Integration

Queue feed events after successful review creation

Build docs developers (and LLMs) love