DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
FREEDOM251

Freedom251 › Blog › API Development

NestJS Choose-Your-Own-Adventure API: Designing Durable Persistence

By Freedom251 Team · · 10 min read

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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

To add persistence to a choose-your-own-adventure API built with NestJS, model the story as related records—adventures, nodes, choices and player sessions—then expose one database client through a Nest provider. A relational database is usually the clearest fit because choices form a graph with foreign-key relationships, while player progress changes independently of the story definition. Prisma is a practical implementation: define the schema declaratively, generate Prisma Client, wrap it in an injectable service, and apply versioned migrations. Keep controllers thin, enforce graph rules in an adventure service, and advance a playthrough inside a transaction so two requests cannot corrupt the player’s current node.

This design turns an in-memory demo into an API that can restart safely, publish repeatable stories, resume sessions and evolve its schema through development and deployment.

As an Amazon Associate I earn from qualifying purchases.

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

Why does a NestJS adventure API need a persistence model?

An in-memory array disappears whenever the process restarts and cannot reliably coordinate multiple API instances. Persistence gives every story element and player state a durable identity, lets readers resume a session, and makes publishing and analytics possible.

NestJS does not force a database choice. The NestJS documentation describes it as database-agnostic and says it can integrate with SQL or NoSQL databases. Its documented integration options include TypeORM, Drizzle, Mongoose, Prisma, MikroORM and Sequelize, as well as direct use of other libraries. That flexibility means the domain model should drive the choice rather than a framework default.

#1 Best Overall
Elebase USB to USB C Adapter for iPhone 18 Pro Max,USBC Car Charger Adapter | 4Pack Type C Female to A Male Cable Converter for Apple Duo 17 16 15 14 Plus,iWatch Watch 12 11 10,iPad 9 8 Air,Airpods 5 4
  • Read Before You Buy — No Video Output: These adapters support charging and USB 2.0 data transfer, but cannot transmit video signals. Except for standard USB webcams (which use USB data only), they are not compatible with HDMI/DisplayPort cables, video-capable USB-C hubs, or docking stations with video output.
  • Convert USB-A Ports to USB-C: Designed to connect USB-C earphones, cables, flash drives, card readers, and other USB-C accessories to standard USB-A ports. Plug-and-play with no drivers or software required.
  • Aluminum Alloy Housing: Built with a sturdy aluminum alloy shell that aids in heat dissipation and protects against daily wear and scratches. Designed to maintain a stable and secure connection.
  • Compact & Travel-Friendly: The ultra-compact design allows the adapter to stay plugged into your device without blocking adjacent ports or adding bulk, reducing wear and tear on your original USB ports.
  • 12-Month Warranty: Backed by a 12-month manufacturer warranty for peace of mind. Designed to meet strict quality control standards for reliable everyday performance.

What should the database schema contain?

Separate immutable story topology from mutable player state. The following relational shape covers the first useful version of the API.

Table Purpose Important columns and rules
Adventure One published or draft story id, title, unique slug, published, createdAt, updatedAt
Node A narrative location in an adventure id, adventureId, stable key, narrative text, terminal; unique (adventureId, key)
Choice A directed transition between nodes id, sourceNodeId, destinationNodeId, label, optional condition
Playthrough Current state for one player or session id, player/session identifier, adventureId, currentNodeId, timestamps
Visit or event Optional history for analytics or replay Playthrough reference, node or choice reference, event time and metadata

A node can have many outgoing choices, and each choice has one destination. Foreign keys should prevent references to deleted nodes. Also validate that a choice’s destination belongs to the same adventure as its source. A simple foreign key cannot always express that cross-row invariant, so enforce it in the service or use a database-specific composite-constraint strategy.

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

Why keep story data separate from playthrough data?

Story records describe what exists; a playthrough describes where one reader currently is. If a node is shared by thousands of sessions, duplicating its text into every session creates inconsistency and makes edits expensive. Keep the session’s current node ID and optional event history instead.

Which indexes matter first?

  • Unique index on Adventure.slug for public lookups.
  • Unique composite index on (adventureId, key) so imports cannot create duplicate node keys.
  • Indexes on Choice.sourceNodeId and Choice.destinationNodeId for graph traversal and validation.
  • Index on the playthrough’s player/session identifier and on currentNodeId for resume requests.

Should you use Prisma or another NestJS database integration?

Prisma is not the only valid answer. Choose by data model, team skills and operational target, then verify connector support for the exact version you pin.

Decision axis Prisma Other Nest-supported options
Model fit Strong fit for the relational adventure graph and explicit relations. TypeORM, Drizzle, MikroORM and Sequelize also target relational models; Mongoose targets documents.
Schema workflow Declarative schema, generated client and SQL migration files that can be customized. Workflows vary: decorators, handwritten SQL, schema builders or ODM models.
Nest integration Expose the generated client through your own injectable provider; there is no Nest-specific Prisma package. Many options provide repository or model injection patterns.
Local database SQLite is useful for a small local project, subject to connector and version support. Other tools may make a different local engine or test setup easier.
Managed deployment Use the provider supported by your pinned Prisma version, such as PostgreSQL where appropriate. Compare hosting, operational tooling and the team’s migration experience.

Prisma’s documented database list includes PostgreSQL, MySQL, SQL Server, SQLite, MongoDB and CockroachDB, but support depends on the Prisma version and connector. Check the current compatibility matrix before committing to a provider. There is no workload-specific benchmark here that proves one ORM is faster for an adventure API.

Rank #2
Sale
Anker USB-C Hub, 5-in-1 USB Hub for Laptops, 4K HDMI Multiport Adapter | USB C Dongle with 90W Max Power Delivery, 3×5Gbps USB A Data Ports, for MacBook, Dell, and More (Charger Not Included)
  • 5-in-1 USB-C Hub: Experience comprehensive connectivity featuring a Power Delivery input, two USB-A 2.0 ports, a USB-A 3.0 port, and an HDMI port. (Note: The USB-C power delivery input port is only for connecting an external wall charger to power your laptop and cannot power peripheral devices.)
  • 90W Pass-Through Charging: Achieve optimal charging with 90W pass-through power to your laptop, supported by a total input of 100W, with the hub reserving 10W for operational efficiency. (Note: Wall charger not included.)
  • Quick Data Transfers: Accelerate your productivity with rapid data transfers using a high-speed 5Gbps USB 3.0 port and two 480Mbps USB 2.0 ports.
  • 4K HDMI Display: Enhance your visual experience with a hub capable of delivering 4K resolution at 30Hz in both mirror and extend modes. Please note that this hub is compatible with MacBook (macOS 12 and newer), Windows 10 and 11, ChromeOS, and laptops equipped with DP Alt Mode and Power Delivery. Note: This device is not compatible with Linux.
  • What You Get: Anker USB-C Hub (5-in-1, 4K HDMI), welcome guide, 18-month warranty, and our friendly customer service.

How do you define the Prisma schema?

Create the domain relationships before writing routes. A compact starting schema can look like this; adapt field types and relation names to the Prisma version and database you install.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
model Adventure {
  id          String         @id @default(cuid())
  title       String
  slug        String         @unique
  published   Boolean        @default(false)
  nodes       Node[]
  playthroughs Playthrough[]
  createdAt   DateTime       @default(now())
  updatedAt   DateTime       @updatedAt
}

model Node {
  id              String   @id @default(cuid())
  adventureId     String
  key             String
  text            String
  terminal        Boolean  @default(false)
  adventure       Adventure @relation(fields: [adventureId], references: [id], onDelete: Cascade)
  outgoingChoices Choice[] @relation("SourceNode")
  incomingChoices Choice[] @relation("DestinationNode")
  playthroughs    Playthrough[]
  @@unique([adventureId, key])
  @@index([adventureId])
}

model Choice {
  id                String @id @default(cuid())
  sourceNodeId      String
  destinationNodeId String
  label             String
  condition         String?
  source            Node   @relation("SourceNode", fields: [sourceNodeId], references: [id], onDelete: Cascade)
  destination       Node   @relation("DestinationNode", fields: [destinationNodeId], references: [id])
  @@index([sourceNodeId])
  @@index([destinationNodeId])
}

model Playthrough {
  id              String   @id @default(cuid())
  adventureId     String
  playerKey       String
  currentNodeId   String
  adventure       Adventure @relation(fields: [adventureId], references: [id], onDelete: Cascade)
  currentNode     Node      @relation(fields: [currentNodeId], references: [id])
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  @@index([playerKey])
  @@index([currentNodeId])
}

The schema intentionally stores a stable node key in addition to the database ID. Imports, editor links and content tools can refer to a key such as forest-entrance without exposing an internal identifier. If you permit cross-adventure references, change the validation rules deliberately; otherwise reject a destination whose adventure ID differs from the source node’s adventure.

How do you expose Prisma Client through NestJS?

The official NestJS Prisma pattern generates Prisma Client, wraps it in an injectable provider and injects that provider into application services. Prisma is an open-source ORM for Node.js and TypeScript, but it has no Nest-specific package that replaces this provider pattern.

Install and generate the client

  1. Install Prisma CLI and the generated client package using the versions selected for the project.
  2. Set DATABASE_URL in the environment used by the API.
  3. Define the models in prisma/schema.prisma.
  4. Run npx prisma generate whenever the schema changes.

Create the database service

Keep connection ownership in a database module. The service below follows the documented lifecycle approach; construct the client with any driver adapter required by the installed Prisma version.

import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Register PrismaService as a provider in a database module and export it for the feature modules that need it. If configuration is injected, read DATABASE_URL through Nest’s configuration system and pass the resulting connection options to the generated client. Keep secrets out of source control.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #3
Sale
Anker USB C Hub, 7in1 Multi-Port USB Adapter, 4K@60Hz USBC to HDMI Splitter | 85W Max PD, 2 USB 3.0 & 1 USBC Data Ports, SD/TF Card Reader, for Laptops/Mac and Other Type C Devices (Charger Not Included)
  • Sleek 7-in-1 USB-C Hub: Features an HDMI port, two USB-A 3.0 ports, and a USB-C data port, each providing 5Gbps transfer speeds. It also includes a USB-C PD input port for charging up to 100W and dual SD and TF card slots, all in a compact design.
  • Flawless 4K@60Hz Video with HDMI: Delivers exceptional clarity and smoothness with its 4K@60Hz HDMI port, making it ideal for high-definition presentations and entertainment. (Note: Only the HDMI port supports video projection; the USB-C port is for data transfer only.)
  • Double Up on Efficiency: The two USB-A 3.0 ports and a USB-C port support a fast 5Gbps data rate, significantly boosting your transfer speeds and improving productivity.
  • Fast and Reliable 85W Charging: Offers high-capacity, speedy charging for laptops up to 85W, so you spend less time tethered to an outlet and more time being productive.
  • What You Get: Anker USB-C Hub (7-in-1), welcome guide, 18-month warranty, and our friendly customer service.

Why use lifecycle hooks?

Connecting during module startup makes a broken database visible when the application boots rather than on the first request. Disconnecting during shutdown lets the process close connections cleanly during deployments, tests and local restarts.

How should controllers and services divide database work?

Controllers should parse DTOs, apply route-level guards and delegate. The adventure service should own Prisma queries and domain rules, including graph validation and conversion of missing records into HTTP errors.

Story-management endpoints

Method and route Expected behavior
POST /adventures Create an adventure and its initial node in one operation or transaction.
POST /adventures/:id/nodes Add a node after confirming the adventure exists and the stable key is unused.
POST /nodes/:id/choices Create a transition only when the destination node belongs to the same adventure.
GET /adventures/:slug Return the published graph, or a documented projection that hides draft-only fields.

Use DTO validation for required strings, booleans and identifiers, but do not rely on DTO validation to prove a destination belongs to the source adventure. That rule requires a database lookup or a transaction that checks both nodes.

Player endpoints

Method and route Expected behavior
POST /playthroughs Create player state at the adventure’s designated start node.
POST /playthroughs/:id/choices/:choiceId Verify that the choice is available from the current node, move the playthrough and return the next node.

Do not let a client submit an arbitrary destination node. The server must resolve the destination through the selected choice from the playthrough’s current node.

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

How do you advance a playthrough safely?

Read the current playthrough and selected choice, verify that the choice starts at the current node, then update the session. When recording an event or visit, perform both writes in one transaction so the state and history cannot disagree.

Rank #4
Sale
UGREEN USB to USB C Adapter Combo 4-Pack, 10Gbps USB C Converter Space Gray | Data transfer & Fast charging for MacBook, iPhone 17/16/15, iPad, Samsung Galaxy, Apple Watch
  • Dual Converters, Infinite Potential:Includes 2× USB C male to USB A female adapters and 2× USB A male to USB C female adapters. Perfect for a wide range of uses—tablets with Bluetooth keyboards, expand USB ports on macbook, and more. Two different converters for all your daily needs
  • Next-Level 10Gbps & 3A Charging: No more slow 480Mbps, this usb to usb c adapter has a transfer speed of up to 10Gbps, allowing you to do more transferring in less time. This usb adapter fits both USB A and USB C charger, supporting up to 3A fast charging
  • Upgraded Exquisite Craftsmanship: With an aluminum alloy housing and metal connector, the usbc to usb adapter is extremely durable and sturdy. Rigorously tested to withstand more than 10,000 times of plugging and unplugging, ensuring long-lasting performance
  • Broad Compatible: The usb c to usb adapter widely supports all USB C/ USB A devices like laptops, tablets, cellphones, car chargers, and phone chargers. Such as compatible with MacBook Pro/Air 2023/2022, Thunderbolt 4/3 Devices,Apple MagSafe Watch 9/8/7/SE/Ultra, iPad Pro 2022/2021, Samsung Galaxy S23/S20/S10, and iPhone 17/16/15 Pro. Plug and play
  • Please Note: To reach 10Gbps speed, keep the cable under 3.3 ft. For USB A Male to USB C adapters, try flipping the USB C connector. USB C Male to USB A adapters support bidirectional 10Gbps transfer within 3.3 ft
  1. Load the playthrough and its current node.
  2. Load the requested choice and its destination.
  3. Reject the request if the playthrough, choice or destination is missing.
  4. Reject it if choice.sourceNodeId is not the playthrough’s currentNodeId.
  5. Check conditions, if your product supports them.
  6. Update currentNodeId and create the visit/event within one transaction.
  7. Return the destination node and its available choices.

A stale client may submit a choice after another request has already advanced the session. Return a conflict or validation error rather than silently applying a transition that is no longer available. Depending on the database, use a transaction isolation level or an update condition that includes the expected current node.

await prisma.$transaction(async (tx) => {
  const playthrough = await tx.playthrough.findUnique({ where: { id } });
  if (!playthrough) throw new NotFoundException();

  const choice = await tx.choice.findUnique({
    where: { id: choiceId },
    include: { destination: true },
  });
  if (!choice || choice.sourceNodeId !== playthrough.currentNodeId) {
    throw new ConflictException('Choice is not available from the current node');
  }

  await tx.playthrough.update({
    where: { id },
    data: { currentNodeId: choice.destinationNodeId },
  });

  await tx.visit.create({
    data: { playthroughId: id, nodeId: choice.destinationNodeId },
  });
});

The example assumes a Visit model and omits authentication and condition evaluation. Add those explicitly rather than treating a session identifier as proof of identity.

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

How do Prisma migrations work in development and deployment?

Prisma Migrate generates SQL migration files from the declarative data model, and the generated SQL can be customized. Treat those files as versioned application assets, review them, commit them and apply the same history in every environment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Define or change models in schema.prisma.
  2. Regenerate the client with npx prisma generate.
  3. Create a named development migration, for example npx prisma migrate dev --name add_playthroughs.
  4. Open the generated SQL and check foreign keys, nullability, destructive operations and indexes.
  5. Commit the migration and schema together.
  6. Run the production-safe migration command for the Prisma version installed by the deployment, rather than using an ad hoc schema push.

Do not use a development reset against a shared or production database. For a destructive change, deploy a compatibility phase first: add the new column or table, backfill it, update application code, then remove obsolete data in a later migration.

When should you seed data?

Seed a small deterministic adventure only in development or another explicitly controlled environment. Production seed scripts should be idempotent, reviewed and separate from migrations when they create editorial content rather than structural data.

Best Value
Sale
Anker USB C Hub, 5-in-1 USBC to HDMI Splitter with 4K Display | 85W Max PD, 1 Powered USB-C 5Gbps & 2 Powered USB-A 3.0 5Gbps Data Ports, for MacBook Pro, MacBook Air, Dell and More (Black)
  • 5-in-1 Connectivity: Equipped with a 4K HDMI port, a 5 Gbps USB-C data port, two 5 Gbps USB-A ports, and a USB C 100W PD-IN port. Note: The USB C 100W PD-IN port supports only charging and does not support data transfer devices such as headphones or speakers.
  • Powerful Pass-Through Charging: Supports up to 85W pass-through charging so you can power up your laptop while you use the hub. Note: Pass-through charging requires a charger (not included). Note: To achieve full power for iPad, we recommend using a 45W wall charger.
  • Transfer Files in Seconds: Move files to and from your laptop at speeds of up to 5 Gbps via the USB-C and USB-A data ports. Note: The USB C 5Gbps Data port does not support video output.
  • HD Display: Connect to the HDMI port to stream or mirror content to an external monitor in resolutions of up to 4K@30Hz. Note: The USB-C ports do not support video output.
  • What You Get: Anker 332 USB-C Hub (5-in-1), welcome guide, our worry-free 18-month warranty, and friendly customer service.

What should you test before calling persistence complete?

  • Restarting the API does not lose adventures or playthroughs.
  • Two nodes in different adventures may reuse the same key, but one adventure may not.
  • A choice cannot point to a node in another adventure.
  • A terminal node exposes no playable choices unless the product intentionally supports post-ending actions.
  • A missing slug, node, choice or playthrough produces the documented HTTP error.
  • Submitting the same stale choice twice cannot move a session twice.
  • A failed event insert rolls back the playthrough update.
  • A migration runs successfully on a clean database and on a copy containing existing rows.
  • The published projection does not leak draft adventures or internal fields.

Which database should you deploy first?

Use SQLite when the project needs a simple local development database and its connector supports the features you require. Use PostgreSQL or another managed engine when you need a shared environment, concurrent API instances, operational backups and stronger production tooling. The right answer depends on the Prisma connector and version you pin, not on a universal performance claim.

Document the chosen provider, connection settings, backup responsibility, migration command and rollback plan in the repository. That operational record matters as much as the model itself when the API moves from a laptop to a deployment pipeline.

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

What is the practical implementation order?

  1. Choose the relational model and write the invariants: unique adventure slugs, unique node keys per adventure and same-adventure transitions.
  2. Add Prisma models and generate the client.
  3. Create the Nest database module and lifecycle-aware PrismaService.
  4. Implement adventure and node creation before exposing player routes.
  5. Add choice creation with destination validation.
  6. Implement playthrough creation at the configured start node.
  7. Implement transactional choice advancement and optional visit events.
  8. Add indexes, migration review and integration tests.
  9. Publish only a projection that makes draft and published behavior explicit.

This sequence keeps persistence concerns behind a service boundary. If you later replace Prisma with another Nest-supported integration, controllers and DTOs can remain stable while repository code changes underneath.

Frequently Asked Questions

Can I use MongoDB for a choose-your-own-adventure API?

Yes. NestJS supports NoSQL integrations, and Prisma lists MongoDB among its supported databases subject to connector and version details. A document model can work, but represent node references and concurrent playthrough updates deliberately rather than embedding a rapidly changing player state inside every story document.

Where should the adventure start node be stored?

A separate start-node reference on Adventure makes the rule explicit and avoids relying on insertion order or a magic node key. Whichever representation you choose, validate that the referenced node belongs to the same adventure before publishing.

How do I handle editing a story that already has active players?

Prefer immutable published revisions or versioned adventures so existing playthroughs continue on the graph they started with. If you edit nodes in place, define whether current sessions see the new choices and test that behavior under concurrent requests.

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

Should conditions be stored as executable code in the database?

No. Store a constrained data representation, such as a condition type and parameters, then evaluate it in application code. Executing arbitrary database strings would create a security and auditing problem.

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.

API Developmentdatabase designMigrationsNestJSPrismaTypeScript

Leave a Reply

Your email address will not be published. Required fields are marked *

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.

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.