Claude Code for Pothos: Code-First GraphQL Schema Builder — Claude Skills 360 Blog
Blog / Backend / Claude Code for Pothos: Code-First GraphQL Schema Builder
Backend

Claude Code for Pothos: Code-First GraphQL Schema Builder

Published: June 3, 2027
Read time: 6 min read
By: Claude Skills 360

Pothos is a code-first GraphQL schema builder for TypeScript — new SchemaBuilder<{ Context: AppContext; Scalars: CustomScalars }>({ plugins: [...] }) creates the builder. builder.objectType("User", { fields: (t) => ({ id: t.exposeID("id"), name: t.exposeString("name") }) }) defines a type. builder.queryField("users", (t) => t.field({ type: ["User"], resolve: async (_root, _args, ctx) => ctx.db.users.findMany() })) adds a query. Input types: builder.inputType("CreateUserInput", { fields: (t) => ({ email: t.string({ required: true }), name: t.string({ required: true }) }) }). builder.mutationField("createUser", (t) => t.field({ type: "User", args: { input: t.arg({ type: CreateUserInput, required: true }) }, resolve: async (_root, { input }, ctx) => createUser(input) })). The relay plugin adds builder.node for cursor pagination with pageInfo and connections. The errors plugin types thrown errors in resolvers. Claude Code generates Pothos schemas, relay connections, input types, and GraphQL server setup.

CLAUDE.md for Pothos

## Pothos Stack
- Version: @pothos/core >= 3.41, @pothos/plugin-relay >= 3.45, @pothos/plugin-errors >= 0.11
- Builder: const builder = new SchemaBuilder<{ Context: AppContext }>({ plugins: [RelayPlugin, ErrorsPlugin] })
- Object: builder.objectType("Post", { fields: (t) => ({ id: t.exposeID("id"), title: t.exposeString("title") }) })
- Query: builder.queryField("post", (t) => t.field({ type: Post, nullable: true, args: { id: t.arg.id({ required: true }) }, resolve: (_, { id }, ctx) => ctx.db.getPost(id) }))
- Mutation: builder.mutationField("createPost", (t) => t.field({ type: Post, args: { input: t.arg({ type: CreatePostInput, required: true }) }, resolve: (_, { input }, ctx) => ctx.db.createPost(input) }))
- Relay node: builder.node("User", { id: { resolve: (user) => user.id }, loadOne: (id, ctx) => ctx.db.getUser(id) })

Schema Builder

// lib/graphql/builder.ts — Pothos schema builder with plugins
import SchemaBuilder from "@pothos/core"
import RelayPlugin from "@pothos/plugin-relay"
import ErrorsPlugin from "@pothos/plugin-errors"
import ValidationPlugin from "@pothos/plugin-validation"
import ScopeAuthPlugin from "@pothos/plugin-scope-auth"
import { db } from "@/lib/db"
import type { InferSelectModel } from "drizzle-orm"
import type { users, posts, tags } from "@/lib/db/schema"

// Context type
export type AppContext = {
  db: typeof db
  userId: string | null
  isAdmin: boolean
}

// DB row types
export type UserRow = InferSelectModel<typeof users>
export type PostRow = InferSelectModel<typeof posts>

export const builder = new SchemaBuilder<{
  Context: AppContext
  Objects: {
    User: UserRow
    Post: PostRow
  }
  AuthScopes: {
    authenticated: boolean
    admin: boolean
  }
  DefaultEdgesNullability: false
}>({
  plugins: [
    ScopeAuthPlugin,
    ErrorsPlugin,
    ValidationPlugin,
    RelayPlugin,
  ],
  relay: {
    clientMutationId: "omit",
    cursorType: "String",
    defaultPageSize: 10,
    maxPageSize: 100,
  },
  authScopes: async (context) => ({
    authenticated: context.userId !== null,
    admin: context.isAdmin,
  }),
  scopeAuthOptions: {
    unauthorizedError: (_, _context, _directive, result) =>
      new Error(result.message ?? "Unauthorized"),
  },
  errorOptions: {
    defaultTypes: [Error],
  },
})

// Top-level error type for mutations
builder.objectType(Error, {
  name: "BaseError",
  fields: (t) => ({
    message: t.exposeString("message"),
  }),
})

Type Definitions

// lib/graphql/types.ts — Pothos type definitions
import { builder, type UserRow, type PostRow } from "./builder"
import { db } from "@/lib/db"
import { users, posts } from "@/lib/db/schema"
import { eq, desc } from "drizzle-orm"
import { z } from "zod"

// ── User type ──────────────────────────────────────────────────────────────

const User = builder.node("User", {
  id: { resolve: (user) => user.id },

  loadOne: async (id, ctx) => {
    return ctx.db.query.users.findFirst({ where: eq(users.id, id) }) ?? null
  },

  fields: (t) => ({
    email: t.exposeString("email"),
    name: t.exposeString("name"),
    role: t.exposeString("role"),
    avatarUrl: t.exposeString("avatarUrl", { nullable: true }),
    createdAt: t.field({
      type: "String",
      resolve: (user) => user.createdAt.toISOString(),
    }),

    posts: t.field({
      type: [Post],
      resolve: async (user, _args, ctx) => {
        return ctx.db.query.posts.findMany({
          where: eq(posts.authorId, user.id),
          orderBy: desc(posts.createdAt),
        })
      },
    }),
  }),
})

// ── Post type ──────────────────────────────────────────────────────────────

const Post = builder.node("Post", {
  id: { resolve: (post) => post.id },

  loadOne: async (id, ctx) => {
    return ctx.db.query.posts.findFirst({ where: eq(posts.id, id) }) ?? null
  },

  fields: (t) => ({
    title: t.exposeString("title"),
    slug: t.exposeString("slug"),
    content: t.exposeString("content"),
    excerpt: t.exposeString("excerpt", { nullable: true }),
    published: t.exposeBoolean("published"),
    readingTime: t.int({
      resolve: (post) => Math.ceil(post.content.split(/\s+/).length / 200),
    }),
    publishedAt: t.field({
      type: "String",
      nullable: true,
      resolve: (post) => post.publishedAt?.toISOString() ?? null,
    }),
    createdAt: t.field({
      type: "String",
      resolve: (post) => post.createdAt.toISOString(),
    }),

    author: t.field({
      type: User,
      resolve: async (post, _args, ctx) => {
        return ctx.db.query.users.findFirst({ where: eq(users.id, post.authorId) })
      },
    }),
  }),
})

// ── Input types ────────────────────────────────────────────────────────────

const CreatePostInput = builder.inputType("CreatePostInput", {
  fields: (t) => ({
    title: t.string({ required: true, validate: { minLength: 5, maxLength: 200 } }),
    content: t.string({ required: true, validate: { minLength: 20 } }),
    excerpt: t.string({ validate: { maxLength: 500 } }),
    published: t.boolean({ defaultValue: false }),
    tags: t.stringList({ defaultValue: [] }),
  }),
})

const UpdatePostInput = builder.inputType("UpdatePostInput", {
  fields: (t) => ({
    title: t.string({ validate: { minLength: 5, maxLength: 200 } }),
    content: t.string({ validate: { minLength: 20 } }),
    excerpt: t.string({ validate: { maxLength: 500 } }),
    published: t.boolean(),
  }),
})

// ── Queries ────────────────────────────────────────────────────────────────

builder.queryFields((t) => ({
  post: t.field({
    type: Post,
    nullable: true,
    args: {
      id: t.arg.id({ required: true }),
    },
    resolve: async (_root, { id }, ctx) => {
      return ctx.db.query.posts.findFirst({ where: eq(posts.id, String(id)) }) ?? null
    },
  }),

  postBySlug: t.field({
    type: Post,
    nullable: true,
    args: {
      slug: t.arg.string({ required: true }),
    },
    resolve: async (_root, { slug }, ctx) => {
      return ctx.db.query.posts.findFirst({ where: eq(posts.slug, slug) }) ?? null
    },
  }),

  // Relay connection for paginated posts
  posts: t.connection({
    type: Post,
    args: {
      search: t.arg.string(),
      published: t.arg.boolean(),
    },
    resolve: async (_root, args, ctx) => {
      // First/after/last/before handled by relay plugin
      const allPosts = await ctx.db.query.posts.findMany({
        orderBy: desc(posts.createdAt),
        where: args.published !== null ? eq(posts.published, args.published ?? true) : undefined,
      })
      return allPosts
    },
  }),

  me: t.field({
    type: User,
    nullable: true,
    authScopes: { authenticated: true },
    resolve: async (_root, _args, ctx) => {
      if (!ctx.userId) return null
      return ctx.db.query.users.findFirst({ where: eq(users.id, ctx.userId) }) ?? null
    },
  }),
}))

// ── Mutations ──────────────────────────────────────────────────────────────

builder.mutationFields((t) => ({
  createPost: t.field({
    type: Post,
    authScopes: { authenticated: true },
    errors: { types: [Error] },
    args: {
      input: t.arg({ type: CreatePostInput, required: true }),
    },
    resolve: async (_root, { input }, ctx) => {
      const slug = input.title
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-|-$/g, "")

      const [post] = await ctx.db
        .insert(posts)
        .values({ ...input, slug, authorId: ctx.userId! })
        .returning()

      return post!
    },
  }),

  updatePost: t.field({
    type: Post,
    authScopes: { authenticated: true },
    errors: { types: [Error] },
    args: {
      id: t.arg.id({ required: true }),
      input: t.arg({ type: UpdatePostInput, required: true }),
    },
    resolve: async (_root, { id, input }, ctx) => {
      const existing = await ctx.db.query.posts.findFirst({
        where: eq(posts.id, String(id)),
      })

      if (!existing) throw new Error("Post not found")
      if (existing.authorId !== ctx.userId) throw new Error("Forbidden")

      const [updated] = await ctx.db
        .update(posts)
        .set(input)
        .where(eq(posts.id, String(id)))
        .returning()

      return updated!
    },
  }),
}))

export { User, Post }

GraphQL Server

// app/api/graphql/route.ts — Yoga GraphQL server with Pothos schema
import { createYoga } from "graphql-yoga"
import { builder } from "@/lib/graphql/builder"
import "@/lib/graphql/types"  // Register all type definitions
import { db } from "@/lib/db"
import { auth } from "@clerk/nextjs/server"
import type { AppContext } from "@/lib/graphql/builder"

const schema = builder.toSchema()

const yoga = createYoga<AppContext>({
  schema,
  graphqlEndpoint: "/api/graphql",
  context: async (req): Promise<AppContext> => {
    const { userId } = await auth()
    return {
      db,
      userId: userId ?? null,
      isAdmin: false,  // Check from DB if needed
    }
  },
})

export { yoga as GET, yoga as POST }

For the TypeGraphQL alternative when class-based decorators (@ObjectType, @Field, @Resolver, @Query, @Mutation) are preferred over a builder pattern — TypeGraphQL uses experimental decorators which are more familiar to NestJS or Java Spring developers while Pothos uses a functional builder API with better type inference, see the TypeGraphQL guide. For the GraphQL Yoga + Nexus alternative when a schema-first approach with nexus.objectType and nexus.makeSchema is preferred — Nexus has similar goals to Pothos but Pothos has better performance and more active maintenance as of 2025, see the Nexus guide. The Claude Skills 360 bundle includes Pothos skill sets covering types, relay pagination, and mutations. Start with the free tier to try GraphQL schema generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free