Full Stack Personal Blog 07: Pothos Modularization and Prisma Integration
cz2025.06.03 14:55

As your project grows and the GraphQL API becomes more complex, manually creating schema and resolvers can reduce development efficiency, as both must have the same structure. Otherwise, errors and unpredictable behavior may occur. When the schema and resolvers change, these two components can easily get out of sync. GraphQL schemas are defined as strings, so for SDL (Schema Definition Language), there is no code auto-completion or build-time error checking.

1. Introduction to GraphQL Pothos

It is recommended to use Pothos to build GraphQL schemas, as it allows you to construct APIs using a programming language, which has several advantages:

  • Perfect integration with TypeScript, providing full type hints during development
  • Supports modular schema definitions, making it easier to manage large GraphQL APIs
  • With the @pothos/plugin-prisma plugin, you can generate GraphQL types directly from Prisma models

2. Practical Application of Pothos

  1. Install dependencies npm install @pothos/plugin-prisma @pothos/core

  2. Update the prisma schema

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}

generator pothos {
  provider = "prisma-pothos-types"
}
  1. Create the Pothos schema builder
// graphql/builder.ts

import SchemaBuilder from '@pothos/core'
import PrismaPlugin from '@pothos/plugin-prisma'
import prisma from '@/lib/prisma'
import { DateTimeResolver } from 'graphql-scalars'

export const builder = new SchemaBuilder<{
  PrismaTypes: any
  Context: {
    prisma: typeof prisma
  }
}>({
  plugins: [PrismaPlugin],
  prisma: {
    client: prisma,
    dmmf: (prisma as any)._dmmf,
  },
})
  1. Redefine the GraphQL Schema

Each model can be separated into its own file, defining its own schema.

// graphql/schema.ts

import { builder } from './builder'

import './types/User'
import './types/Post'
export const schema = builder.toSchema()
  1. Refactor the API endpoint
// src/api/graphql/route.ts
import { createYoga } from 'graphql-yoga'
import { schema } from '../../../../graphql/schema'
import { NextRequest } from 'next/server'

const { handleRequest } = createYoga({
  schema,
})

export async function GET(request: NextRequest) {
  return handleRequest(request, {} as any)
}

export async function POST(request: NextRequest) {
  return handleRequest(request, {} as any)
}
  1. Redefine query and mutation interfaces

With the help of Pothos' Prisma plugin, you can use prismaObject to directly reference field types defined in Prisma, with automatic editor hints. You can also use objectType to define new object types.

// types/Post.ts
import prisma from '@/lib/prisma'
import { builder } from '../builder'

builder.prismaObject('Post', {
  fields: (t: any) => ({
    id: t.exposeID('id'),
    title: t.exposeString('title'),
    summary: t.exposeString('summary'),
    content: t.exposeString('content'),
    createdAt: t.expose('createdAt', { type: 'DateTime' }),
    updatedAt: t.expose('updatedAt', { type: 'DateTime' }),
    createdById: t.exposeID('createdById'),
    createdBy: t.relation('createdBy'),
  }),
})

// Define paginated result type
builder.objectType('PaginatedPosts' as any, {
  fields: (t) => ({
    posts: t.field({
      type: ['Post'] as any,
      resolve: (parent) => parent.posts,
    }),
    postsCount: t.int({
      resolve: (parent) => parent.postsCount,
    }),
  }),
})

// Define query types
builder.queryType({
  fields: (t) => ({
    // Query a single post
    post: t.field({
      type: 'Post' as any,
      args: {
        id: t.arg.id(),
      },
      resolve: async (_root: any, args: any, ctx: any) => {
        const { id } = args
        const post = await ctx.prisma.post.findUnique({
          where: { id },
        })
        return post
      },
    }),
    // Paginated query
    paginatedPosts: t.field({
      type: 'PaginatedPosts' as any,
      args: {
        skip: t.arg.int({ defaultValue: 0 }),
        take: t.arg.int({ defaultValue: 10 }),
      },
      resolve: async (_root: any, args: any, ctx: any) => {
        const { skip, take } = args

        // Get total count
        const totalCount = await ctx.prisma.post.count()

        // Get paginated data
        const posts = await ctx.prisma.post.findMany({
          skip: skip || 0,
          take: take || 10,
          orderBy: {
            id: 'desc', // Sort by ID in descending order, newest first
          },
        })

        return {
          posts,
          postsCount: totalCount,
        }
      },
    } as any),
  }),
})

// Define mutation type for adding a post
builder.mutationType({
  fields: (t: any) => ({
    addPost: t.field({
      type: 'Post' as any,
      args: {
        title: t.arg.string(),
        summary: t.arg.string(),
        content: t.arg.string(),
      },
      resolve: async (_root: any, args: any, ctx: any) => {
        if (ctx.user?.role !== 'ADMIN') {
          throw new Error('you are not admin')
        }
        const { title, summary, content } = args

        const post = await ctx.prisma.post.create({
          data: { title, summary, content, createdById: ctx.user.id },
        })

        return post
      },
    }),
  }),
} as any)

Comments