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 client — builder.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 builder — t.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 change — npx 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