Prisma
Back to blog

The new way to use Prisma ORM 8 in NestJS

Ankur Datta
Ankur Datta
September 24, 2026
Part 15 of 15 in the Prisma 8 series.View full series →

Prisma ORM 8, the next major version of Prisma's TypeScript ORM, changes how you wire the client into NestJS. The Prisma ORM 8 client is a plain object exported by src/prisma/db.ts, so there is no class to extend, and the NestJS template that create-prisma scaffolds has no service that extends PrismaClient. One shared client lives in src/prisma/db.ts, and an injectable PrismaService exposes query helpers written as ordinary functions. Prisma ORM 8 is available today as a release candidate.

This post is the fifteenth in our Prisma 8 series, following Is Prisma 8 ready for long-lived production apps?. It walks through scaffolding a NestJS API on Prisma Postgres, following a request from the controller to the database, writing typed queries, and deploying the API to Prisma Compute. We ran every command below against prisma@8.0.0-rc.15, @prisma/orm-postgres@8.0.0-rc.11, and NestJS 11.

Share your client without extending it

If you have used Prisma with NestJS, you have probably written a service that extends PrismaClient. In Prisma ORM 7, the setup commonly looked like this:

import { Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client";

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  constructor() {
    super({
      adapter: new PrismaPg({
        connectionString: process.env.DATABASE_URL!,
      }),
    });
  }

  async onModuleInit() {
    await this.$connect();
  }
}

That works for a basic client, but adding client extensions is where the class pattern gets awkward: $extends() returns an extended instance, while the service inherits from the original class. NestJS users have reported this for years in issue 18628.

Prisma ORM 8 removes the class. The client is the object that postgres({ ... }) returns in src/prisma/db.ts, so there is no PrismaClient to inherit from. $extends is gone too: cross-cutting behavior is middleware that you pass as the middleware option when you create the client, so there is no extended instance to keep in sync with a subclass. The NestJS starter wraps the client in a small injectable service and exposes the queries your application needs:

import { Injectable } from "@nestjs/common";
import { db, listUsers, type StarterUser } from "./prisma/users";

@Injectable()
export class PrismaService {
  readonly db = db;

  listUsers(limit = 10): Promise<StarterUser[]> {
    return listUsers(limit);
  }
}

NestJS injects PrismaService, and the service delegates to functions in src/prisma/. You keep query helpers in ordinary TypeScript files and call them from the service. You could wrap PrismaClient in a service before. What changes in Prisma ORM 8 is that there is no class and no generated package: the client is a value in src/prisma/db.ts, and the middleware that replaced $extends is an option you pass when you create it.

db.ts first asks the Prisma Composer service binding for a database client. Outside Composer that lookup throws, so it falls back to postgres({ contractJson, url: process.env.DATABASE_URL }). Locally you use the fallback. On Prisma Compute the binding wins.

There is also no generated client package. Prisma ORM 7 generated one into an output directory, and importing it from NestJS raised recurring questions about import paths and pnpm type errors. Prisma ORM 8 emits the contract into src/prisma/ as contract.json and contract.d.ts, and db.ts imports them with relative paths.

1. Create the NestJS project

You need Node.js 24.11 or newer, or 22.18 or newer on the 22 line. Then run:

npm create prisma@latest -- nest-prisma-8 --template nest --provider postgres --yes
cd nest-prisma-8

This creates the NestJS app, installs Prisma ORM 8, writes the contract to src/prisma/contract.prisma (what Prisma ORM 7 called schema.prisma) with its emitted contract.json and contract.d.ts, and adds module.ts and service.ts for deployment.

--yes accepts the scaffold defaults and answers no to the deploy prompt. You deploy explicitly at the end. The defaults also install Prisma agent skill files into .claude/, .cursor/, .agents/, and .devin/, plus a postinstall script that keeps them in sync. Pass --skills none to skip that.

The app already has a /users endpoint. These are the files to look at:

FileWhat it does
src/users.controller.tsHandles HTTP requests to /users
src/users.service.tsCalls the query exposed by PrismaService
src/prisma.service.tsMakes the shared client and query helpers injectable
src/app.module.tsRegisters both controllers and both providers
src/prisma/users.tsHolds the starter listUsers query, which runs the seed first
src/prisma/seed.tsUpserts three sample users, once per process
src/prisma/db.tsCreates the shared database client

contract.json and contract.d.ts are emitted from contract.prisma. After you change the contract, run npm run contract:emit to regenerate them, then npm run db:update to apply the change to the database.

You can also choose a different contract authoring style, either TypeScript or Prisma Schema Language, or a different package manager through the create-prisma options.

2. Connect to Prisma Postgres and initialize the database

Create a temporary Prisma Postgres database:

npx create-db@latest

The command prints a connection string and a claim link. The database is deleted after 24 hours unless you open the claim link and keep it.

Copy the connection string, export it in your terminal, then apply the starter contract to the database:

export DATABASE_URL="<your PostgreSQL connection string>"
npm run db:init

The scaffold ships an .env.example that tells you to copy it to .env. Do not rely on that for this walkthrough: neither prisma.config.ts nor the CLI loads .env, so db:init and npm run dev only see a variable you exported in the shell.

db:init ends with Applied 5 operation(s) across 1 contract space and lists the tables, constraints, and indexes it created. If it stops with Connection terminated unexpectedly, the new database is still starting. Wait a few seconds and run it again.

There is no seed command. listUsers upserts three sample users the first time it runs in a process, so the data appears on the first request to /users.

3. Follow a request from controller to database

AppModule registers PrismaService and UsersService, and UsersService receives the shared Prisma service through NestJS dependency injection:

import { Inject, Injectable } from "@nestjs/common";
import { PrismaService } from "./prisma.service";

@Injectable()
export class UsersService {
  constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}

  async findAll() {
    return this.prisma.listUsers(10);
  }
}

The controller calls that service when a request arrives:

import { Controller, Get, Inject } from "@nestjs/common";
import { UsersService } from "./users.service";

@Controller("users")
export class UsersController {
  constructor(@Inject(UsersService) private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

The explicit @Inject(...) decorators name each provider token. The starter runs tsx watch in development, and tsx transpiles with esbuild, which ignores the emitDecoratorMetadata flag in tsconfig.json. Without the metadata, NestJS cannot infer a constructor parameter's provider from its type, so the token is spelled out and injection behaves the same in development and in the tsdown production build. This applies to code you add too: put @Inject(Token) on every injected constructor parameter.

Start the app:

npm run dev

In a second terminal, call the endpoint:

curl http://localhost:3000/users

You get the starter users as JSON. The request goes through the controller, the service, and the Prisma query helper, with one shared client behind them.

4. Write queries for the endpoint

Prisma ORM 8 queries chain the conditions you need, then execute with .all() or .first().

For example, a helper that fetches a short list of user ids and emails can look like this:

import { db } from "./db";

export async function listUserIdsAndEmails() {
  return db.orm.public.User.select("id", "email")
    .orderBy((user) => user.email.asc())
    .limit(10)
    .all();
}

Here, public is the PostgreSQL schema and User is the model.

Selecting two fields also narrows the result type to those fields. A service can call this helper the same way it calls the starter's listUsers function. Helpers you write yourself do not call seed(), so on a fresh database request /users once before you expect rows from them. See the reading data guide for filtering and pagination.

When you need lower-level control, the same client exposes a SQL builder:

const query = db.sql.public.user
  .select("id", "email")
  .where((fields, operators) => operators.ilike(fields.email, "%@prisma.io"))
  .limit(10)
  .build();

const users = await db.runtime().query(query);

The SQL builder is keyed by table name: the model name with a lowercase first letter, unless the model sets @@map. That is why the ORM path says User and the SQL path says user.

The builder sends %@prisma.io as a query parameter, so the pattern is never concatenated into the SQL text. You can use the SQL builder for one endpoint and the ORM everywhere else; both run on the same client and pool. The advanced queries guide covers joins, the SQL builder, and the MongoDB pipeline builder.

The client is lazy: postgres({ ... }) builds the query surface up front and opens the driver pool the first time a query runs. The starter's seed() calls connectDatabase() in src/prisma/db.ts to open that pool explicitly before its upserts. Nothing closes the pool. Keep that one client shared across requests. To release connections on shutdown, implement OnModuleDestroy on PrismaService and call db.close() there, then enable shutdown hooks with app.enableShutdownHooks() in main.ts:

import { Injectable, type OnModuleDestroy } from "@nestjs/common";
import { db } from "./prisma/db";
import { listUsers, type StarterUser } from "./prisma/users";

@Injectable()
export class PrismaService implements OnModuleDestroy {
  readonly db = db;

  listUsers(limit = 10): Promise<StarterUser[]> {
    return listUsers(limit);
  }

  async onModuleDestroy() {
    await this.db.close();
  }
}

If you are coming from Prisma ORM 7, the Prisma ORM 7 to 8 upgrade guide shows how contracts and the new query APIs fit together, one route at a time.

Deploy the API to Prisma Compute

The scaffold includes module.ts and service.ts, which describe the app and its infrastructure through Prisma Composer, which is in early access. The Prisma CLI can deploy that declaration to Prisma Compute.

npx prisma auth login
npm run build
npx prisma deploy module.ts

prisma auth login opens the browser once. npm run build bundles the app with tsdown into dist/server.mjs. prisma deploy module.ts provisions the database and the compute service and prints the app URL. npm run deploy runs the last two together.

nest-prisma-8
├─ database   postgres-database db_abc123
└─ app        compute-service cps_abc123
              https://xyz.ewr.prisma.build

The deploy does not copy your local database. prisma deploy provisions a Prisma Postgres database for the module and hands it to the app through the Composer binding, so the deployed app never reads your local DATABASE_URL. The first request to /users seeds it.

Take the app URL printed by the CLI and check it:

curl https://<your-app-host>/users

You now have a NestJS API on Prisma Compute and its database on Prisma Postgres. For the full deployment walkthrough, see the NestJS guide in the docs. For previews per Git branch and deploy-on-push, see Deploy on push.

Prisma ORM 8 supports MongoDB too

NestJS with MongoDB was a recurring question on Prisma ORM 7. Prisma ORM 8 supports MongoDB in early access: document models in the contract, a pipeline builder for aggregations, and migration plans that create collections and indexes.

Start a separate NestJS project with the MongoDB provider:

npm create prisma@latest -- nest-mongo --template nest --provider mongodb --package-manager pnpm --yes

With npm, the current create-prisma release stops at an ERESOLVE peer-dependency error: @prisma/orm-mongo requires the MongoDB 7 driver, while alchemy, the deployment library Prisma Composer depends on, still declares mongodb@^6. pnpm resolves it, so the command above pins pnpm.

The scaffold writes .env with mongodb://localhost:27017/mydb?replicaSet=rs0&directConnection=true, but nothing loads that file. MongoDB projects use two variables: the Prisma CLI scripts such as db:init read MONGODB_URL, and the dev server in src/prisma/db.ts still reads DATABASE_URL. On a standalone mongod, drop replicaSet=rs0 and export both before pnpm run db:init and pnpm run dev:

export MONGODB_URL="mongodb://localhost:27017/mydb?directConnection=true"
export DATABASE_URL="$MONGODB_URL"

Prisma ORM 8 does not run MongoDB transactions yet. If you need them, use the driver's sessions directly. Transactions need a replica set, so in that case keep replicaSet=rs0 and run mongod as a replica set.

The MongoDB quickstart walks through the connection variables, the migration plan, and the first query.

Frequently asked questions

Building with NestJS? Tell us how it goes

Try Prisma ORM 8 in your NestJS app and tell us what works well or where you get stuck. Join our Discord to report what breaks and ask questions. Release-candidate feedback goes straight to the ORM team.

About the author

Ankur Datta
Ankur Datta

Ankur is a member of the Prisma team who works closely with the developer community, with hundreds of contributions across Prisma's open source repositories and a background that includes founding an ed-tech startup. He writes about TypeScript, Node.js, PostgreSQL, and modern application stacks.

Keep reading

Build your next app with Prisma

Start free. Scale when you’re ready.

Try Prisma
Share this article