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
// 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 },
}})
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'),
}),
})
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,
}),
}),
}))
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,
},
})
},
}),
}))
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 }
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 }
}
}
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)