DEV Community

Atlas Whoff
Atlas Whoff

Posted on

GraphQL with Pothos and Prisma: Type-Safe Schema-First API Development

GraphQL with Pothos and Prisma: Type-Safe Schema-First API Development

Pothos is a code-first GraphQL schema builder that integrates directly with Prisma.
No code generation. No resolver type mismatches. Here's how it works.

Setup

npm install @pothos/core @pothos/plugin-prisma graphql graphql-yoga
Enter fullscreen mode Exit fullscreen mode
// lib/builder.ts
import SchemaBuilder from '@pothos/core'
import PrismaPlugin from '@pothos/plugin-prisma'
import { prisma } from './prisma'
import type PrismaTypes from '@pothos/plugin-prisma/generated'

export const builder = new SchemaBuilder<{
  PrismaTypes: PrismaTypes
  Context: { userId: string | null }
>({{
  plugins: [PrismaPlugin],
  prisma: { client: prisma },
}})
Enter fullscreen mode Exit fullscreen mode

Defining Types

// types/User.ts
import { builder } from '../lib/builder'

builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
    email: t.exposeString('email'),
    createdAt: t.expose('createdAt', { type: 'DateTime' }),
    // Relation
    posts: t.relation('posts'),
    // Computed field
    postCount: t.relationCount('posts'),
  }),
})

builder.prismaObject('Post', {
  fields: (t) => ({
    id: t.exposeID('id'),
    title: t.exposeString('title'),
    content: t.exposeString('content', { nullable: true }),
    published: t.exposeBoolean('published'),
    author: t.relation('author'),
  }),
})
Enter fullscreen mode Exit fullscreen mode

Queries

// resolvers/queries.ts
builder.queryFields((t) => ({
  me: t.prismaField({
    type: 'User',
    nullable: true,
    resolve: (query, _root, _args, ctx) => {
      if (!ctx.userId) return null
      return prisma.user.findUnique({
        ...query,  // Pothos adds the right select/include
        where: { id: ctx.userId },
      })
    },
  }),

  posts: t.prismaField({
    type: ['Post'],
    args: {
      published: t.arg.boolean({ required: false }),
      take: t.arg.int({ defaultValue: 20 }),
    },
    resolve: (query, _root, args) =>
      prisma.post.findMany({
        ...query,
        where: args.published != null ? { published: args.published } : {},
        take: args.take,
      }),
  }),
}))
Enter fullscreen mode Exit fullscreen mode

Mutations

const CreatePostInput = builder.inputType('CreatePostInput', {
  fields: (t) => ({
    title: t.string({ required: true }),
    content: t.string(),
  }),
})

builder.mutationFields((t) => ({
  createPost: t.prismaField({
    type: 'Post',
    args: { input: t.arg({ type: CreatePostInput, required: true }) },
    resolve: (query, _root, { input }, ctx) => {
      if (!ctx.userId) throw new Error('Unauthorized')
      return prisma.post.create({
        ...query,
        data: {
          title: input.title,
          content: input.content,
          authorId: ctx.userId,
        },
      })
    },
  }),
}))
Enter fullscreen mode Exit fullscreen mode

Server Setup (graphql-yoga)

// app/api/graphql/route.ts
import { createYoga } from 'graphql-yoga'
import { schema } from '@/lib/schema'
import { getServerSession } from 'next-auth'

const { handleRequest } = createYoga({
  schema,
  context: async () => {
    const session = await getServerSession()
    return { userId: session?.user?.id ?? null }
  },
  fetchAPI: { Request, Response },
})

export { handleRequest as GET, handleRequest as POST }
Enter fullscreen mode Exit fullscreen mode

N+1 Query Prevention

Pothos automatically handles this — the ...query spread tells Prisma exactly what
to join based on the GraphQL query shape. No DataLoader needed for Prisma relations.

# This does ONE query with the right joins, not N+1:
query {
  posts {
    title
    author { name email }
  }
}
Enter fullscreen mode Exit fullscreen mode

When GraphQL Over tRPC

  • Public API that third parties consume
  • Complex nested data with varying query shapes
  • Need subscriptions

For internal Next.js APIs: tRPC is simpler. For public APIs or mobile clients: GraphQL.


The AI SaaS Starter Kit includes API route patterns for both tRPC and REST — ready to extend to GraphQL. $99 one-time.

Top comments (0)