Setup

Setting up the Prisma plugin

npm install --save @pothos/plugin-prisma

Setup

The Prisma plugin uses generated types to describe your models and relations. Add its generator alongside your Prisma client generator, then pass the generated types and datamodel to the builder.

Add the pothos generator to your prisma schema

generator pothos {
  provider = "prisma-pothos-types"
}

Now the types Pothos uses will be generated whenever you re-generate your prisma client. Run the following command to re-generate the client and create the new types:

npx prisma generate

Generator options:

  • clientOutput: Where the generated code will import the PrismaClient from. The default is the full path of wherever the client is generated. If you are checking in the generated file, you should specify a relative path for this import
  • output: Where to write the generated types

Example with more options:


generator client {
  provider      = "prisma-client"
  output        = "../lib/prisma"
}
generator pothos {
  provider = "prisma-pothos-types"
  clientOutput = "./prisma" // relative path from pothos output to prisma client
  output = "../lib/pothos-prisma-types.ts"
}

If model or relation completions are missing, check the client import in the generated file.

Set up the builder

This example uses the generated client above and a SQLite database. Install @prisma/adapter-better-sqlite3 for this adapter; use the adapter for your database if it differs. exposeDescriptions also accepts { models: true, fields: true } to configure descriptions separately.

import SchemaBuilder from '@pothos/core';
import { PrismaClient } from '../lib/prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import PrismaPlugin from '@pothos/plugin-prisma';

import type PrismaTypes from '../lib/pothos-prisma-types'; // path to generated types, specified in your prisma.schema
import { getDatamodel } from '../lib/pothos-prisma-types';

const prisma = new PrismaClient({
  adapter: new PrismaBetterSqlite3({ url: 'file:./dev.db' }),
});

const builder = new SchemaBuilder<{
  PrismaTypes: PrismaTypes;
  Context: { userId: number };
}>({
  plugins: [PrismaPlugin],
  prisma: {
    client: prisma,
    // This give pothos information about your tables, relations, and indexes to help it generate optimal queries at runtime.
    // This used to be attached to the prisma client, but has been removed in most runtimes/modes to reduce bundle size.
    dmmf: getDatamodel(),
    // defaults to false, uses /// comments from prisma schema as descriptions
    // for object types, relations and exposed fields.
    // descriptions can be omitted by setting description to false
    exposeDescriptions: false,
    // use where clause from prismaRelatedConnection for totalCount (defaults to true)
    filterConnectionTotalCount: true,
    // warn when not using a query parameter correctly
    onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn',
    // leave selections inside @defer fragments out of the planned query (defaults to true)
    skipDeferredFragments: true,
  },
});

The examples use an authenticated request context with userId: number. Supply it through your GraphQL server; see Context.

Pass the Prisma client through the plugin options. Including its full type in Context can slow TypeScript checking; see this TypeScript issue.

You can also load or create the prisma client dynamically for each request. This can be used to periodically re-create clients or create read-only clients for certain types of users.

Replace the builder above with the following to select between prisma and a second client. This SQLite example uses READ_ONLY_REPLICA_URL for a replica database maintained by your application; the adapter does not configure replication or enforce read-only access. Configure database access permissions separately. Both clients must use the same generated Prisma client and schema. For another database, use its driver adapter and replica connection options.

const replicaUrl = process.env.READ_ONLY_REPLICA_URL;
if (!replicaUrl) {
  throw new Error('READ_ONLY_REPLICA_URL is required');
}

const readOnlyPrisma = new PrismaClient({
  adapter: new PrismaBetterSqlite3({ url: replicaUrl }),
});

const builder = new SchemaBuilder<{
  Context: { user: { isAdmin: boolean } };
  PrismaTypes: PrismaTypes;
}>({
  plugins: [PrismaPlugin],
  prisma: {
    client: (ctx) => (ctx.user.isAdmin ? prisma : readOnlyPrisma),
    dmmf: getDatamodel(),
  },
});

Detecting unused query arguments

Forgetting to spread the query argument from t.prismaField or t.prismaConnection into your prisma query can result in inefficient queries, or even missing data. To help catch these issues, the plugin can warn you when you are not using the query argument correctly.

The onUnusedQuery option can be set to warn or error to enable this feature. When set to warn it will log a warning to the console if Pothos detects that you have not properly used the query in your resolver. Similarly if you set the option to error it will throw an error instead. You can also pass a function which will receive the info object which can be used to log or throw your own error.

The check tracks access to properties on the query object. If no properties are accessed on the query object before the resolver returns, it will trigger the onUnusedQuery condition.

It's recommended to enable this check in development to more quickly find potential issues.

Deferred fragments

skipDeferredFragments controls query planning; it does not enable incremental execution. The application must register the defer directive and use an executor, server transport, and client that support the same incremental delivery protocol. With GraphQL.js 17, ordinary execute rejects schemas containing @defer or @stream; incremental execution uses experimentalExecuteIncrementally. For a server integration, see GraphQL Yoga's defer and stream setup, which uses @graphql-yoga/plugin-defer-stream. Check the integration's supported versions when choosing an executor and client; adding directives or changing skipDeferredFragments alone is not sufficient.

Selections inside a @defer fragment are left out of the planned query by default, so the initial payload is not delayed by data the client has agreed to wait for. When the deferred fragment resolves, its fields are loaded through fallback queries, batched as usual.

Set skipDeferredFragments: false in the plugin options to plan deferred selections with the rest of the query. queryFromInfo accepts the same option per call.

Run the publishing API

The publishing example uses this SQLite adapter and generated client with one schema for author pages, private drafts, media attachments, and pagination. Prisma Utils adds filters and draft inputs to that same schema. The example's schema.prisma defines User, Profile, Post, Comment, Media, and PostMedia, including the foreign keys and unique constraints needed by their relations.

From a checkout of the Pothos repository, with Node.js 22 or newer:

pnpm install --frozen-lockfile
pnpm --dir website check:local
npm --prefix website/local-examples run prisma -- author

The setup generates both clients and SQLite DDL from the Prisma schema. Each run creates a fresh temporary database and removes it afterward. The output contains the GraphQL response and emitted SQL. The example guide lists the operations and suggested edits. Applications should use Prisma migrations for their persistent databases.

The request's userId comes from authentication in an application. The local runner supplies it as a fixture. Public author fields return published posts; private viewer fields return only the current author's drafts. Ownership and publication checks also apply to draft updates and node refetches. Selecting a GraphQL type does not by itself enforce those restrictions.