User Reading Shelf API

Introduction: Why Track Reading Progress?

Welcome back! In the previous lesson, you learned how to build a book catalog and search for books in your reading tracker API. Now, let’s take the next step: tracking your own reading progress and organizing your personal shelf.

Imagine you want to see which books you’ve finished, which ones you’re still reading, and which you want to read next. This is where per-user reading status and the shelf API come in. By the end of this lesson, you’ll understand how to store and update your reading progress and how to view your shelf with filters and sorting options.

How Per-User Reading Status Works

Each user can have their own reading progress for each book. This is stored in the ReadingSession object. The key fields are:

  • userId: Whose progress is this?
  • bookId: Which book?
  • currentPage: What page is the user on?
  • status: What is the reading status? (optional, but useful)

The status can be one of:

  • "not-started"
  • "in-progress"
  • "completed"
  • "want-to-read"

Let’s look at how a reading session is updated in the service:

// src/reading/reading.service.ts (excerpt)
  updateProgress(dto: UpdateProgressDto) {
    // Ensure user and book exist
    // Validate user exists
    if (!this.db.findUserById(dto.userId)) {
      throw new NotFoundException('User not found');
    }
    const book: any = this.booksService.findOne(dto.bookId);

    let session = this.db
      .getReadingSessions()
      .find((s: any) => s.userId === dto.userId && s.bookId === dto.bookId);

    const normalized =
      dto.status === 'want-to-read'
        ? { currentPage: 0, status: 'want-to-read' as const }
        : { currentPage: dto.currentPage, status: dto.status ?? this.deriveStatus(dto.currentPage, book.totalPages) };

    if (session) {
      session.currentPage = normalized.currentPage;
      (session as any).status = normalized.status;
      (session as any).updatedAt = new Date().toISOString();
    } else {
      session = { userId: dto.userId, bookId: dto.bookId, currentPage: normalized.currentPage, status: normalized.status, updatedAt: new Date().toISOString() };
      this.db.getReadingSessions().push(session);
    }
    return session;
  }

What it does
Upserts a user’s reading session for a given book and normalizes status. The method guarantees the user/book exist, derives status when not explicitly provided, and stamps updatedAt so the client can order shelf activity.

Execution flow

  1. Existence checksusersService.findOne(dto.userId) and booksService.findOne(dto.bookId) throw if the user/book doesn’t exist, preventing orphan sessions.
  2. Normalization
    • If dto.status === 'want-to-read', the method forces { currentPage: 0, status: 'want-to-read' }, ignoring any incoming currentPage. This models intent (“queue this book”) and prevents phantom progress.
    • Otherwise, it sets currentPage = dto.currentPage and status = dto.status ?? deriveStatus(currentPage, totalPages).
    • deriveStatus rules: currentPage <= 0 → 'not-started'; currentPage >= totalPages (with totalPages > 0) → 'completed'; else 'in-progress'.
  3. Upsert — Looks up an existing session by (userId, bookId).
    • If found, it mutates currentPage, status, and updatedAt.
    • If absent, it pushes a new session with those fields plus userId, bookId.
  4. Return — Returns the persisted session object (useful for immediate UI reconciliation).

Explanation:

  • The method checks if a reading session already exists for the user and book.
  • If it does, it updates the progress and status.
  • If not, it creates a new session.
  • The status is set based on the current page and total pages, or directly from the user’s input.

Example Output:

Suppose Alice (userId: 2) updates her progress on "The Hobbit" (bookId: 1) to page 50:

{
  "userId": 2,
  "bookId": "1",
  "currentPage": 50,
  "status": "in-progress",
  "updatedAt": "2024-06-01T12:00:00.000Z"
}
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal