Skip to content

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

FixDevs ·

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.

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.

For related GraphQL issues, see Fix: GraphQL Yoga Not Working and Fix: tRPC Not Working.

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