Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

This part of the Grimoire API tutorial turns story-choice metadata into game behavior: choices award XP, a service derives the player’s level from total XP, and another service awards badges when a player reaches a qualifying page. Keeping these responsibilities separate from progress persistence makes the rules easier to understand and test.

What changes in Part 3

The earlier installments established a read-only story endpoint and persisted player progress. In this installment, the story choices’ existing xpReward and optional badgeUnlocked values begin to affect play. The implementation adds dedicated game-logic services rather than accumulating XP and badge conditionals in ProgressService. The tutorial by Rodolphe D. uses NestJS providers for this logic, consistent with NestJS guidance that controllers handle requests and delegate complex work to providers. NestJS controllers and providers describe those roles.

Calculate level from total XP

The tutorial’s XpService calculates a level from accumulated XP using a project-specific threshold: level N begins at N² × 100 XP. The resulting examples are:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Level threshold Total XP needed to reach it
Level 1 100 XP
Level 2 400 XP
Level 3 900 XP

Consequently, the tutorial describes level 1 as applying below 400 XP, level 2 as beginning at 400 XP, and level 3 as beginning at 900 XP. Its xpForNextLevel example returns the threshold for the next level. These are choices for this project, not a standard game-progression curve; adjust the rule to suit the experience you want to create.

Why derive level instead of storing it?

XP is the persisted value; level is derived from that total. This avoids maintaining two independently mutable values that could disagree after a reward or correction. The XP calculation can remain a pure function: give it a total and it returns a level or next threshold without querying the database. That separation makes threshold boundaries straightforward to test.

Award badges without repeating them

The tutorial’s BadgesService checks for an existing player-badge record before creating one. In the sequential flow shown, a repeat call finds the prior record and returns without issuing the same badge again. This rule belongs in its own service because it depends on persisted player-badge state, unlike the pure XP calculation.

A read-then-insert check alone can still race if two requests for the same player and badge arrive concurrently: both can observe no record before either inserts. For a production implementation, enforce uniqueness for the player-and-badge pair in the database, or use an equivalent concurrency-safe mechanism, and handle a duplicate attempt cleanly. The tutorial’s illustrated check does not demonstrate such a constraint.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Apply rewards when a player chooses

When a player advances, the example adds the chosen option’s XP reward, updates the current page, saves progress, and then checks the destination page for a badge. This connects the story’s existing choice and page metadata to player state.

  1. Confirm that the requested choice is available on the player’s current page. Reject an unavailable choice rather than applying its reward.
  2. Read the chosen option’s XP reward and add it to the player’s total.
  3. Set the player’s current page to the choice’s destination.
  4. Persist the updated progress.
  5. Check the destination page’s badge rule and award the badge if the player does not already have it.

Validating and normalizing the caller-supplied choice identifier at the request boundary is useful, but validation alone does not prove the choice belongs to the player’s current page. The game logic should check that relationship before applying a reward. NestJS documents class-based ValidationPipe, StandardSchemaValidationPipe for compatible schemas, and parsing pipes for individual values. NestJS validation

Give invalid choices a clear HTTP error

The tutorial introduces InvalidChoiceException, derived from NestJS BadRequestException. That gives the domain failure a meaningful name in application code while retaining a 400-class HTTP response for a bad choice. The request handler can therefore distinguish an invalid selection from failures such as a database error.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Keep account identity and storage choices in scope

The series uses PostgreSQL with TypeORM, but neither choice is a requirement of NestJS or of the game rules. NestJS documents integrations with TypeORM, Drizzle, Mongoose, Prisma, MikroORM, and Sequelize. NestJS database integration documentation

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Part 3 focuses on game mechanics, not account authorization. The following installment is described as replacing the temporary default player with authenticated-account scoping. In a deployed API, do not let a caller select another player’s progress merely by supplying an identifier: establish identity through authentication and authorize access to the corresponding progress. NestJS guards inspect request execution context and run after middleware but before interceptors or pipes. NestJS guards

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.