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.
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:
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
- Existence checks —
usersService.findOne(dto.userId)andbooksService.findOne(dto.bookId)throw if the user/book doesn’t exist, preventing orphan sessions. - Normalization
- If
dto.status === 'want-to-read', the method forces{ currentPage: 0, status: 'want-to-read' }, ignoring any incomingcurrentPage. This models intent (“queue this book”) and prevents phantom progress. - Otherwise, it sets
currentPage = dto.currentPageandstatus = dto.status ?? deriveStatus(currentPage, totalPages). deriveStatusrules:currentPage <= 0 → 'not-started';currentPage >= totalPages (with totalPages > 0) → 'completed'; else'in-progress'.
- If
- Upsert — Looks up an existing session by
(userId, bookId).- If found, it mutates
currentPage,status, andupdatedAt. - If absent, it pushes a new session with those fields plus
userId,bookId.
- If found, it mutates
- 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:
Now, let’s see how to build the endpoint that lets a user view their personal shelf. This endpoint will show all books the user is tracking, along with their progress and status.
Here’s the main part of the controller and service:
What it does
Builds the current user’s shelf by joining reading sessions with book metadata, computing normalized status and a numeric progress, then applying optional filtering (status) and sorting (title, author, updatedAt, progress) with a specified order.
Execution flow
- Validate & prepare — Confirms the user exists, loads all books once, and constructs a
Map<bookId, Book>for O(1) lookups. - Compose items — For each of the user’s sessions:
- Fetch the corresponding book.
- Compute
total = book.totalPagesandstatus = session.status ?? deriveStatus(currentPage, total). This ensures older sessions without stored status still behave correctly. - Compute
progress = total ? currentPage / total : 0. (This yields a 0–1 ratio in normal cases; if pages exceed total, the ratio can be >1—clamp in UI if desired.) - Emit an enriched item:
{ bookId, title, author, totalPages, currentPage, status, progress, updatedAt }.
- Filter — If
query.statusis present, keep only items with that status (e.g.,'want-to-read'). - Sort — Uses stable numeric/string comparators:
title/author: locale-awarelocaleCompare.progress: numeric ascending/descending.updatedAt: string compare on ISO timestamps;nullis treated as'', so it sorts first in ascending, last in descending.- Default
orderis'asc'when not specified.
Why it returns enriched items (not raw sessions) Shelf consumers (UI/tests) need immediately renderable data: book identity for display, normalized status for badges, and a numeric progress for bars/sorting. Returning a pre-joined, computed structure keeps clients thin and guarantees consistent derivation rules across views.
Explanation:
- The endpoint returns all reading sessions for the current user.
- Each session is enriched with book details and progress.
- The results can be filtered by status and sorted by different fields.
You can customize your shelf view using query parameters. Here are some examples:
-
Filter by status:
/reading/shelf?status=completed
Shows only books you have completed. -
Sort by title (descending):
/reading/shelf?sortBy=title&order=desc
Shows your shelf sorted by book title, from Z to A. -
Sort by progress:
/reading/shelf?sortBy=progress&order=asc
Shows books with the least progress first.
Example Output:
Suppose Alice has two books on her shelf:
If Alice requests /reading/shelf?status=want-to-read, she will see only "Dune" in the results.
In this lesson, you learned how to track each user’s reading progress and status, and how to build an API endpoint that lets users view and organize their personal shelf. You saw how to filter and sort your shelf using query parameters, making it easy to find the books you want to focus on.
Next, you’ll get to practice these concepts by updating your own reading progress and exploring your shelf with different filters and sorting options. This hands-on practice will help you become comfortable with managing per-user reading status and using the shelf API effectively.
