Skip to content

Fix: Pothos Not Working — Types Not Resolving, Plugin Errors, or Prisma Integration Failing

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix Pothos GraphQL schema builder issues — type-safe schema definition, object and input types, Prisma plugin, relay connections, auth scope plugin, and schema printing.

The Problem

A Pothos type reference doesn’t resolve:

const builder = new SchemaBuilder({});

builder.queryType({
  fields: (t) => ({
    user: t.field({
      type: 'User',  // Error: Type "User" has not been implemented
      resolve: () => getUser(),
    }),
  }),
});

Or the Prisma plugin doesn’t generate the expected types:

builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
  }),
});
// Error: Unknown model "User"

Or the schema builds but queries return type errors at runtime:

Cannot return null for non-nullable field Query.users

Why This Happens

Pothos is a code-first GraphQL schema builder for TypeScript. It generates schemas from TypeScript code instead of SDL (Schema Definition Language):

  • Types must be defined before referencing them — Pothos uses a builder pattern. If you reference 'User' in a query before defining the User type with builder.objectType, the schema fails to build. Types can be defined in any file, but all must be imported before builder.toSchema().
  • The Prisma plugin needs the generated Prisma clientbuilder.prismaObject('User', ...) maps to Prisma models. The plugin reads the Prisma schema at build time. If the Prisma client isn’t generated (npx prisma generate), model types are unknown.
  • Nullable vs non-nullable is strict — Pothos enforces GraphQL nullability at the type level. If a resolver returns null for a non-nullable field (t.field without nullable: true), GraphQL throws at runtime.
  • Plugins add methods to the buildert.prismaField, t.authField, t.relayConnection only exist after enabling the corresponding plugin. Using them without the plugin causes “not a function” errors.

A second class of issues comes from the TypeScript layer. Pothos infers everything from your SchemaBuilder generic argument: scalars, context, plugin types, Prisma types. If your tsconfig.json has strict: false or noImplicitAny: false, Pothos may “build” successfully but produce wrong types at runtime — fields are typed as any and silently misalign. Pothos assumes strict TypeScript and falls apart without it.

A third class is the Prisma generator. The Pothos Prisma plugin requires prisma-pothos-types to be added to your schema.prisma as a generator block. If you forget the generator or use an older Prisma version, the generated types file at @pothos/plugin-prisma/generated is missing or stale. The error “Unknown model” usually means the generator block is missing or the build did not pick up a schema change.

Version History (Pothos v3 → v4, Prisma plugin updates, and the older nexus/type-graphql era)

The code-first GraphQL schema ecosystem in TypeScript went through several waves before Pothos became the default choice.

  • Nexus (2018-2021) — GraphQL Nexus from Prisma Labs was the first popular code-first schema builder for TypeScript. It used a DSL with separate type definitions and plugins for Prisma. Nexus development slowed after Prisma deprecated its Nexus plugin in favor of TypeGraphQL alternatives, leaving many projects stranded.
  • TypeGraphQL (2018→) — class-and-decorators approach. Powerful but requires experimentalDecorators and emitDecoratorMetadata, and the runtime type inference relies on metadata reflection that breaks in some bundlers. Still actively maintained but considered heavier than Pothos.
  • Pothos v1 (2021) — Michael Hayes (then giraphql) released the first version under the name GiraphQL with a focus on plugin composition and type inference without decorators. It was renamed Pothos in v2 to avoid the awkward name.
  • Pothos v2 (late 2021) — the rename, official Prisma plugin, relay plugin, and scope-auth plugin. v2 codified the “tiny core + many plugins” architecture that Pothos still follows.
  • Pothos v3 (2022) — improved type inference, better support for federated schemas (@pothos/plugin-federation), and t.expose* helpers for terse field definitions. Most tutorials and GitHub examples reference v3 syntax.
  • Pothos v4 (August 2023) — significant Prisma plugin overhaul (t.prismaConnection improvements, better N+1 handling via lookahead, support for Prisma’s preview features). The builder configuration changed slightly: plugin options are now passed via dedicated keys instead of being merged into the top-level config. Most v3 schemas migrate to v4 with small adjustments.
  • Pothos v4 schema builder ecosystem (2024-2026) — Pothos replaced the older Nexus / TypeGraphQL ecosystem as the default code-first choice. It pairs naturally with GraphQL Yoga, Prisma, and Drizzle. The Prisma plugin in particular benefits from each Prisma minor release; staying within one minor of the latest Prisma is the safest bet.

Practical implication: if you find code using builder.objectRef('User') and t.field({ type: User }), that is v3 style and still works in v4. If you find code that passes plugin options directly into the SchemaBuilder constructor, check the v4 migration guide — most options moved into a per-plugin key like prisma: { client } or scopeAuth: { ... }. If a project still uses Nexus or TypeGraphQL and you are evaluating migration, Pothos is the most idiomatic target for a fresh code-first schema.

Fix 1: Basic Schema Building

npm install @pothos/core graphql
// lib/schema/builder.ts — create the builder
import SchemaBuilder from '@pothos/core';

interface Context {
  currentUser: { id: string; role: string } | null;
  db: typeof db;
}

export const builder = new SchemaBuilder<{
  Context: Context;
  Scalars: {
    DateTime: { Input: Date; Output: Date };
    ID: { Input: string; Output: string };
  };
}>({});

// Register custom scalars
builder.scalarType('DateTime', {
  serialize: (date) => date.toISOString(),
  parseValue: (value) => new Date(value as string),
});
// lib/schema/types/user.ts — define User type
import { builder } from '../builder';

// Define the User object type
builder.objectType('User', {
  description: 'A registered user',
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
    email: t.exposeString('email'),
    role: t.exposeString('role'),
    createdAt: t.expose('createdAt', { type: 'DateTime' }),
    // Computed field
    displayName: t.string({
      resolve: (user) => `${user.name} (${user.role})`,
    }),
    // Relationship
    posts: t.field({
      type: ['Post'],
      resolve: async (user, _, ctx) => {
        return ctx.db.query.posts.findMany({
          where: eq(posts.authorId, user.id),
        });
      },
    }),
  }),
});

// Define the Post type
builder.objectType('Post', {
  fields: (t) => ({
    id: t.exposeID('id'),
    title: t.exposeString('title'),
    body: t.exposeString('body'),
    published: t.exposeBoolean('published'),
    author: t.field({
      type: 'User',
      resolve: async (post, _, ctx) => {
        return ctx.db.query.users.findFirst({
          where: eq(users.id, post.authorId),
        });
      },
    }),
  }),
});
// lib/schema/queries.ts — define queries
import { builder } from './builder';

builder.queryType({
  fields: (t) => ({
    users: t.field({
      type: ['User'],
      resolve: async (_, __, ctx) => {
        return ctx.db.query.users.findMany();
      },
    }),
    user: t.field({
      type: 'User',
      nullable: true,  // Can return null if not found
      args: {
        id: t.arg.id({ required: true }),
      },
      resolve: async (_, args, ctx) => {
        return ctx.db.query.users.findFirst({
          where: eq(users.id, args.id),
        });
      },
    }),
    posts: t.field({
      type: ['Post'],
      args: {
        limit: t.arg.int({ defaultValue: 20 }),
        offset: t.arg.int({ defaultValue: 0 }),
      },
      resolve: async (_, args, ctx) => {
        return ctx.db.query.posts.findMany({
          limit: args.limit!,
          offset: args.offset!,
        });
      },
    }),
  }),
});
// lib/schema/mutations.ts
import { builder } from './builder';

// Input type
const CreateUserInput = builder.inputType('CreateUserInput', {
  fields: (t) => ({
    name: t.string({ required: true }),
    email: t.string({ required: true }),
    role: t.string({ defaultValue: 'user' }),
  }),
});

builder.mutationType({
  fields: (t) => ({
    createUser: t.field({
      type: 'User',
      args: {
        input: t.arg({ type: CreateUserInput, required: true }),
      },
      resolve: async (_, { input }, ctx) => {
        const [user] = await ctx.db.insert(users).values(input).returning();
        return user;
      },
    }),
    deleteUser: t.field({
      type: 'Boolean',
      args: { id: t.arg.id({ required: true }) },
      resolve: async (_, { id }, ctx) => {
        await ctx.db.delete(users).where(eq(users.id, id));
        return true;
      },
    }),
  }),
});
// lib/schema/index.ts — build the schema
import { builder } from './builder';

// Import all type/query/mutation files to register them
import './types/user';
import './queries';
import './mutations';

// Build the schema
export const schema = builder.toSchema();

Fix 2: Prisma Plugin

npm install @pothos/plugin-prisma
npx prisma generate  # Required — generates PrismaClient types
// lib/schema/builder.ts — with Prisma plugin
import SchemaBuilder from '@pothos/core';
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '@pothos/plugin-prisma/generated';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export const builder = new SchemaBuilder<{
  PrismaTypes: PrismaTypes;
  Context: { prisma: typeof prisma; currentUser: User | null };
}>({
  plugins: [PrismaPlugin],
  prisma: { client: prisma },
});
// Types automatically mapped from Prisma schema
builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
    email: t.exposeString('email'),
    // Prisma relation — auto-resolved
    posts: t.relation('posts', {
      args: { published: t.arg.boolean() },
      query: (args) => ({
        where: args.published != null ? { published: args.published } : {},
        orderBy: { createdAt: 'desc' },
      }),
    }),
    // Count
    postCount: t.relationCount('posts'),
  }),
});

builder.prismaObject('Post', {
  fields: (t) => ({
    id: t.exposeID('id'),
    title: t.exposeString('title'),
    body: t.exposeString('body'),
    published: t.exposeBoolean('published'),
    author: t.relation('author'),
    createdAt: t.expose('createdAt', { type: 'DateTime' }),
  }),
});

// Queries with Prisma
builder.queryField('users', (t) =>
  t.prismaField({
    type: ['User'],
    resolve: async (query, _, __, ctx) => {
      // `query` includes select/include from nested field selections
      return ctx.prisma.user.findMany({ ...query });
    },
  }),
);

builder.queryField('user', (t) =>
  t.prismaField({
    type: 'User',
    nullable: true,
    args: { id: t.arg.id({ required: true }) },
    resolve: async (query, _, args, ctx) => {
      return ctx.prisma.user.findUnique({
        ...query,
        where: { id: args.id },
      });
    },
  }),
);

Fix 3: Auth Scope Plugin

npm install @pothos/plugin-scope-auth
import SchemaBuilder from '@pothos/core';
import ScopeAuthPlugin from '@pothos/plugin-scope-auth';

export const builder = new SchemaBuilder<{
  Context: { currentUser: User | null };
  AuthScopes: {
    isLoggedIn: boolean;
    isAdmin: boolean;
  };
}>({
  plugins: [ScopeAuthPlugin],
  authScopes: async (context) => ({
    isLoggedIn: !!context.currentUser,
    isAdmin: context.currentUser?.role === 'admin',
  }),
});

// Protected query
builder.queryField('me', (t) =>
  t.field({
    type: 'User',
    authScopes: { isLoggedIn: true },
    resolve: (_, __, ctx) => ctx.currentUser!,
  }),
);

// Admin-only mutation
builder.mutationField('deleteUser', (t) =>
  t.field({
    type: 'Boolean',
    authScopes: { isAdmin: true },
    args: { id: t.arg.id({ required: true }) },
    resolve: async (_, { id }, ctx) => {
      await ctx.db.delete(users).where(eq(users.id, id));
      return true;
    },
  }),
);

// Protected type — all fields require auth
builder.objectType('SecretData', {
  authScopes: { isAdmin: true },
  fields: (t) => ({
    key: t.exposeString('key'),
    value: t.exposeString('value'),
  }),
});

Fix 4: Relay Plugin (Cursor Pagination)

npm install @pothos/plugin-relay
import RelayPlugin from '@pothos/plugin-relay';

const builder = new SchemaBuilder<{ /* ... */ }>({
  plugins: [RelayPlugin],
  relay: {},
});

// Node interface — enables `node(id: "...")` query
builder.prismaNode('User', {
  id: { field: 'id' },
  fields: (t) => ({
    name: t.exposeString('name'),
    email: t.exposeString('email'),
    posts: t.relatedConnection('posts', { cursor: 'id' }),
  }),
});

// Connection query — cursor-based pagination
builder.queryField('users', (t) =>
  t.prismaConnection({
    type: 'User',
    cursor: 'id',
    resolve: (query, _, __, ctx) => {
      return ctx.prisma.user.findMany({ ...query });
    },
  }),
);

// Query:
// query { users(first: 10, after: "cursor") { edges { node { name } } pageInfo { hasNextPage endCursor } } }

Fix 5: Print Schema to SDL

import { printSchema } from 'graphql';
import { schema } from './lib/schema';
import fs from 'fs';

// Generate schema.graphql for client codegen
const sdl = printSchema(schema);
fs.writeFileSync('schema.graphql', sdl);
console.log('Schema written to schema.graphql');
// package.json
{
  "scripts": {
    "schema:generate": "tsx scripts/print-schema.ts"
  }
}

Fix 6: Use with GraphQL Yoga

// app/api/graphql/route.ts — Pothos + Yoga + Next.js
import { createYoga } from 'graphql-yoga';
import { schema } from '@/lib/schema';
import { auth } from '@/auth';

const yoga = createYoga({
  schema,
  context: async ({ request }) => {
    const session = await auth();
    return {
      currentUser: session?.user || null,
      prisma,
      db,
    };
  },
  graphqlEndpoint: '/api/graphql',
  fetchAPI: { Response },
});

export { yoga as GET, yoga as POST, yoga as OPTIONS };

Still Not Working?

“Type X has not been implemented” — the type definition file isn’t imported. Pothos registers types when their definition code executes. Import all type files in your schema/index.ts before calling builder.toSchema(). Order doesn’t matter — just import them.

Prisma plugin shows “Unknown model” — run npx prisma generate to create the PrismaClient types. The Pothos Prisma plugin reads from the generated types at @pothos/plugin-prisma/generated. If you renamed or added models, regenerate.

Non-nullable field returns null — the resolver returned null or undefined for a field without nullable: true. Either make the field nullable or ensure the resolver always returns a value. For relationships, this often means the related record doesn’t exist.

Circular type references cause TypeScript errors — Pothos handles circular references (User → Posts → User) through lazy evaluation with () => ... wrappers. If TypeScript complains, use builder.objectRef('User') to create a forward reference.

Migrating from v3 to v4 breaks plugin configuration — v4 moved many plugin options out of the top-level SchemaBuilder config into dedicated keys. Read the v4 migration guide and search your codebase for keys that used to live at the root (scopeAuth, relay, prisma) — they now nest under their plugin name.

Generated Prisma types are stale after a schema changenpx prisma generate produces the type file the Pothos plugin reads. If you forgot to add the prisma-pothos-types generator block to schema.prisma, the generated file does not include the model union Pothos needs. Add the generator block, regenerate, and restart your TypeScript server.

N+1 queries despite using t.relation — the Prisma plugin uses lookahead to merge selections into a single query, but only inside t.prismaField and t.prismaConnection. If you use a plain t.field and resolve relations manually, you bypass the optimization. Move to t.prismaField or use DataLoader for non-Prisma resolvers.

For related GraphQL and database issues, see Fix: GraphQL Yoga Not Working and Fix: tRPC Not Working. For Prisma-specific issues that show up through Pothos, see Fix: Prisma N+1 Query Problem and Fix: Prisma Connection Pool Exhausted.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles