Introduction

Welcome to the final lesson of this course! So far, you have learned how to build a book catalog, track user reading progress, and add sorting and analytics to your API. In this lesson, you will learn how to provide user-specific statistics in your reading tracker API. User statistics help users see their reading progress, such as how many pages they have read or how many books they have finished. By the end of this lesson, you will know how to integrate different modules and expose a new endpoint that returns these statistics for any user.

Users And Reading Modules

Before we dive in, let’s briefly remind ourselves how the Users and Reading modules are set up and connected. In previous lessons, you created separate modules for users and for tracking reading sessions. Each module has its own service and controller. The Users module manages user data, while the Reading module keeps track of what each user is reading.

Here’s a quick look at how modules are imported and used together:

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { ReadingModule } from '../reading/reading.module';

@Module({
  imports: [ReadingModule],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

By importing ReadingModule, the Users layer can call reading.findAllForUser(userId) to obtain sessions as ground truth for statistics. This avoids duplicating persistence logic and keeps the graph of dependencies explicit and testable.

Building The User Statistics Feature

Now, let’s build the feature that calculates user statistics. The main method for this is getStats in the UsersService. Here’s the code:

  // src/users/users.service.ts
  getStats(userId: string) {
    this.findOne(userId);
    const sessions = this.reading.findAllForUser(userId);
    const books = this.db.getBooks() as any[];

    const totalPagesRead = sessions.reduce((sum: number, s: any) => sum + s.currentPage, 0);
    let booksCompleted = 0;
    let denom = 0;
    for (const s of sessions as any[]) {
      const b = books.find((bb: any) => bb.id === s.bookId);
      if (!b || !b.totalPages) continue;
      denom += b.totalPages;
      if (s.currentPage >= b.totalPages) booksCompleted += 1;
    }
    const avgProgressPct = denom > 0 ? totalPagesRead / denom : 0;

    return {
      userId,
      totalPagesRead,
      booksInShelf: sessions.length,
      booksCompleted,
      avgProgressPct,
    };
  }

Let’s break down what this method does:

  • this.findOne(userId): Checks if the user exists. If not, it throws an error.
  • this.reading.findAllForUser(userId): Gets all reading sessions for the user.
  • this.db.getBooks(): Gets the list of all books.
  • totalPagesRead: Adds up the current page from each reading session to get the total number of pages the user has read.
  • The loop checks each session to see if the user has finished the book (currentPage >= totalPages). It also keeps track of the total number of pages across all books in the user’s shelf.
  • avgProgressPct: Calculates the average progress as a percentage by dividing the total pages read by the total number of pages in all books the user is reading.
  • The method returns an object with all these statistics.

Example Output:

Suppose a user has read 120 pages in one book and finished another book of 200 pages. The output might look like:

{
  "userId": "someId",
  "totalPagesRead": 320,
  "booksInShelf": 2,
  "booksCompleted": 1,
  "avgProgressPct": 0.8
}

This means the user has read a total of 320 pages, has 2 books in their shelf, completed 1 book, and has an average progress of 80%.

This design keeps derivation server-side so all clients share consistent semantics. The normalized avgProgressPct (0–1) makes users comparable regardless of book lengths, and booksCompleted gives a discrete, motivating milestone. The method reads only in-memory data exposed by DatabaseService and ReadingService, keeping dependencies minimal.

Adding The User Statistics Endpoint

To make these statistics available through the API, you need to add a new endpoint in the Users controller. Here’s how you do it:

// src/users/users.controller.ts
  // NEW: user stats (auth required by default global guard)
@Get(':id/stats')
@UseGuards(JwtAuthGuard)
getStats(@Param('id', ParseUUIDPipe) id: string) {
 const data = this.usersService.getStats(id);
 return { success: true, data };
}

This code adds a new route, /users/:id/stats, that returns the statistics for the user with the given ID. When a client sends a GET request to this endpoint, it will receive the statistics object we just discussed.

Example Request:

GET /users/1/stats

Example Response:

{
  "userId": "someId",
  "totalPagesRead": 320,
  "booksInShelf": 2,
  "booksCompleted": 1,
  "avgProgressPct": 0.8
}

This makes it easy for users to see their reading progress at any time.

Test the feature end-to-end (Unit-4–scoped CURLs)
Summary And Next Steps

In this lesson, you learned how to integrate the Reading module with the Users module to provide user-specific statistics. You saw how to inject one service into another, calculate useful statistics, and expose them through a new API endpoint. This is a powerful way to give users insight into their reading habits.

Congratulations on reaching the end of the course! You now have the skills to build advanced features in a reading tracker API, including filtering, aggregation, and sorting. Take some time to try the practice exercises that follow to reinforce what you’ve learned. Well done!

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