GraphQL Pattern

Build type-safe GraphQL APIs with Apollo Server, schema definitions, resolvers, DataLoader for N+1 prevention, and client-side integration.

Overview#

GraphQL provides a flexible query language for APIs, allowing clients to request exactly the data they need. This pattern covers setting up a complete GraphQL API with Apollo Server in Next.js.

When to use:

  • Complex data requirements with nested relationships
  • Multiple clients needing different data shapes
  • Real-time features with subscriptions
  • APIs requiring flexible queries

Key features:

  • Schema-first development
  • Type-safe resolvers
  • DataLoader for N+1 query prevention
  • Subscriptions for real-time updates
  • Apollo Client integration

Code Example#

Apollo Server Setup#

1// app/api/graphql/route.ts 2import { ApolloServer } from '@apollo/server' 3import { startServerAndCreateNextHandler } from '@as-integrations/next' 4import { typeDefs } from '@/graphql/schema' 5import { resolvers } from '@/graphql/resolvers' 6import { createContext } from '@/graphql/context' 7 8const server = new ApolloServer({ 9 typeDefs, 10 resolvers 11}) 12 13const handler = startServerAndCreateNextHandler(server, { 14 context: createContext 15}) 16 17export { handler as GET, handler as POST }

Schema Definition#

1// graphql/schema.ts 2import { gql } from 'graphql-tag' 3 4export const typeDefs = gql` 5 type User { 6 id: ID! 7 email: String! 8 name: String 9 posts: [Post!]! 10 createdAt: String! 11 } 12 13 type Post { 14 id: ID! 15 title: String! 16 content: String 17 published: Boolean! 18 author: User! 19 comments: [Comment!]! 20 createdAt: String! 21 } 22 23 type Comment { 24 id: ID! 25 content: String! 26 author: User! 27 post: Post! 28 createdAt: String! 29 } 30 31 type Query { 32 me: User 33 user(id: ID!): User 34 users(limit: Int, offset: Int): [User!]! 35 post(id: ID!): Post 36 posts(published: Boolean, limit: Int, offset: Int): [Post!]! 37 } 38 39 type Mutation { 40 createUser(input: CreateUserInput!): User! 41 updateUser(id: ID!, input: UpdateUserInput!): User! 42 deleteUser(id: ID!): Boolean! 43 44 createPost(input: CreatePostInput!): Post! 45 updatePost(id: ID!, input: UpdatePostInput!): Post! 46 deletePost(id: ID!): Boolean! 47 publishPost(id: ID!): Post! 48 49 createComment(input: CreateCommentInput!): Comment! 50 deleteComment(id: ID!): Boolean! 51 } 52 53 input CreateUserInput { 54 email: String! 55 name: String 56 password: String! 57 } 58 59 input UpdateUserInput { 60 email: String 61 name: String 62 } 63 64 input CreatePostInput { 65 title: String! 66 content: String 67 published: Boolean 68 } 69 70 input UpdatePostInput { 71 title: String 72 content: String 73 published: Boolean 74 } 75 76 input CreateCommentInput { 77 postId: ID! 78 content: String! 79 } 80`

Resolvers#

1// graphql/resolvers.ts 2import { prisma } from '@/lib/db' 3import { GraphQLError } from 'graphql' 4import { Context } from './context' 5 6export const resolvers = { 7 Query: { 8 me: async (_: unknown, __: unknown, ctx: Context) => { 9 if (!ctx.user) return null 10 return prisma.user.findUnique({ where: { id: ctx.user.id } }) 11 }, 12 13 user: async (_: unknown, { id }: { id: string }) => { 14 return prisma.user.findUnique({ where: { id } }) 15 }, 16 17 users: async (_: unknown, { limit = 20, offset = 0 }: { limit?: number; offset?: number }) => { 18 return prisma.user.findMany({ take: limit, skip: offset }) 19 }, 20 21 post: async (_: unknown, { id }: { id: string }) => { 22 return prisma.post.findUnique({ where: { id } }) 23 }, 24 25 posts: async (_: unknown, args: { published?: boolean; limit?: number; offset?: number }) => { 26 return prisma.post.findMany({ 27 where: args.published !== undefined ? { published: args.published } : undefined, 28 take: args.limit ?? 20, 29 skip: args.offset ?? 0, 30 orderBy: { createdAt: 'desc' } 31 }) 32 } 33 }, 34 35 Mutation: { 36 createPost: async ( 37 _: unknown, 38 { input }: { input: { title: string; content?: string; published?: boolean } }, 39 ctx: Context 40 ) => { 41 if (!ctx.user) { 42 throw new GraphQLError('Not authenticated', { 43 extensions: { code: 'UNAUTHENTICATED' } 44 }) 45 } 46 47 return prisma.post.create({ 48 data: { 49 ...input, 50 authorId: ctx.user.id 51 } 52 }) 53 }, 54 55 updatePost: async ( 56 _: unknown, 57 { id, input }: { id: string; input: Partial<{ title: string; content: string; published: boolean }> }, 58 ctx: Context 59 ) => { 60 const post = await prisma.post.findUnique({ where: { id } }) 61 62 if (!post) { 63 throw new GraphQLError('Post not found', { 64 extensions: { code: 'NOT_FOUND' } 65 }) 66 } 67 68 if (post.authorId !== ctx.user?.id) { 69 throw new GraphQLError('Not authorized', { 70 extensions: { code: 'FORBIDDEN' } 71 }) 72 } 73 74 return prisma.post.update({ 75 where: { id }, 76 data: input 77 }) 78 }, 79 80 deletePost: async (_: unknown, { id }: { id: string }, ctx: Context) => { 81 const post = await prisma.post.findUnique({ where: { id } }) 82 83 if (!post || post.authorId !== ctx.user?.id) { 84 throw new GraphQLError('Not authorized', { 85 extensions: { code: 'FORBIDDEN' } 86 }) 87 } 88 89 await prisma.post.delete({ where: { id } }) 90 return true 91 } 92 }, 93 94 // Field resolvers for relations 95 User: { 96 posts: (parent: { id: string }) => { 97 return prisma.post.findMany({ where: { authorId: parent.id } }) 98 } 99 }, 100 101 Post: { 102 author: (parent: { authorId: string }) => { 103 return prisma.user.findUnique({ where: { id: parent.authorId } }) 104 }, 105 comments: (parent: { id: string }) => { 106 return prisma.comment.findMany({ where: { postId: parent.id } }) 107 } 108 }, 109 110 Comment: { 111 author: (parent: { userId: string }) => { 112 return prisma.user.findUnique({ where: { id: parent.userId } }) 113 }, 114 post: (parent: { postId: string }) => { 115 return prisma.post.findUnique({ where: { id: parent.postId } }) 116 } 117 } 118}

Context#

1// graphql/context.ts 2import { NextRequest } from 'next/server' 3import { auth } from '@/auth' 4 5export interface Context { 6 user: { id: string; email: string } | null 7} 8 9export async function createContext({ req }: { req: NextRequest }): Promise<Context> { 10 const session = await auth() 11 12 return { 13 user: session?.user ? { 14 id: session.user.id!, 15 email: session.user.email! 16 } : null 17 } 18}

DataLoader for N+1 Prevention#

1// graphql/loaders.ts 2import DataLoader from 'dataloader' 3import { prisma } from '@/lib/db' 4import { User, Post } from '@prisma/client' 5 6export function createLoaders() { 7 return { 8 userLoader: new DataLoader<string, User | null>(async (ids) => { 9 const users = await prisma.user.findMany({ 10 where: { id: { in: [...ids] } } 11 }) 12 13 const userMap = new Map(users.map(u => [u.id, u])) 14 return ids.map(id => userMap.get(id) ?? null) 15 }), 16 17 userPostsLoader: new DataLoader<string, Post[]>(async (userIds) => { 18 const posts = await prisma.post.findMany({ 19 where: { authorId: { in: [...userIds] } } 20 }) 21 22 const postsByUser = new Map<string, Post[]>() 23 posts.forEach(post => { 24 const existing = postsByUser.get(post.authorId) ?? [] 25 postsByUser.set(post.authorId, [...existing, post]) 26 }) 27 28 return userIds.map(id => postsByUser.get(id) ?? []) 29 }) 30 } 31} 32 33// Updated context 34export interface Context { 35 user: { id: string; email: string } | null 36 loaders: ReturnType<typeof createLoaders> 37} 38 39// Updated resolvers using loaders 40export const resolvers = { 41 Post: { 42 author: (parent: { authorId: string }, _: unknown, ctx: Context) => { 43 return ctx.loaders.userLoader.load(parent.authorId) 44 } 45 }, 46 User: { 47 posts: (parent: { id: string }, _: unknown, ctx: Context) => { 48 return ctx.loaders.userPostsLoader.load(parent.id) 49 } 50 } 51}

Client-Side Usage#

1// lib/graphql/client.ts 2import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client' 3import { setContext } from '@apollo/client/link/context' 4 5const httpLink = createHttpLink({ 6 uri: '/api/graphql' 7}) 8 9const authLink = setContext((_, { headers }) => { 10 const token = typeof window !== 'undefined' 11 ? localStorage.getItem('token') 12 : null 13 14 return { 15 headers: { 16 ...headers, 17 authorization: token ? `Bearer ${token}` : '' 18 } 19 } 20}) 21 22export const apolloClient = new ApolloClient({ 23 link: authLink.concat(httpLink), 24 cache: new InMemoryCache() 25}) 26 27// hooks/usePosts.ts 28import { gql, useQuery, useMutation } from '@apollo/client' 29 30const GET_POSTS = gql` 31 query GetPosts($limit: Int, $offset: Int) { 32 posts(limit: $limit, offset: $offset) { 33 id 34 title 35 content 36 published 37 author { 38 id 39 name 40 } 41 } 42 } 43` 44 45const CREATE_POST = gql` 46 mutation CreatePost($input: CreatePostInput!) { 47 createPost(input: $input) { 48 id 49 title 50 } 51 } 52` 53 54export function usePosts(limit = 10) { 55 const { data, loading, error, fetchMore } = useQuery(GET_POSTS, { 56 variables: { limit, offset: 0 } 57 }) 58 59 const loadMore = () => { 60 fetchMore({ 61 variables: { offset: data?.posts.length ?? 0 } 62 }) 63 } 64 65 return { posts: data?.posts ?? [], loading, error, loadMore } 66} 67 68export function useCreatePost() { 69 const [createPost, { loading, error }] = useMutation(CREATE_POST, { 70 refetchQueries: [{ query: GET_POSTS }] 71 }) 72 73 return { createPost, loading, error } 74}

Subscriptions with WebSocket#

1// graphql/schema.ts (add subscription type) 2const typeDefs = gql` 3 type Subscription { 4 postCreated: Post! 5 commentAdded(postId: ID!): Comment! 6 } 7` 8 9// graphql/resolvers.ts 10import { PubSub } from 'graphql-subscriptions' 11 12const pubsub = new PubSub() 13 14export const resolvers = { 15 Mutation: { 16 createPost: async (_: unknown, { input }: any, ctx: Context) => { 17 const post = await prisma.post.create({ 18 data: { ...input, authorId: ctx.user!.id } 19 }) 20 21 pubsub.publish('POST_CREATED', { postCreated: post }) 22 23 return post 24 } 25 }, 26 27 Subscription: { 28 postCreated: { 29 subscribe: () => pubsub.asyncIterableIterator(['POST_CREATED']) 30 }, 31 commentAdded: { 32 subscribe: (_: unknown, { postId }: { postId: string }) => { 33 return pubsub.asyncIterableIterator([`COMMENT_ADDED_${postId}`]) 34 } 35 } 36 } 37}

Usage Instructions#

  1. Set up Apollo Server: Create the GraphQL endpoint with Next.js integration
  2. Define schema: Write your GraphQL type definitions
  3. Implement resolvers: Create resolver functions for queries and mutations
  4. Add context: Set up authentication and loaders in context
  5. Implement DataLoaders: Prevent N+1 queries with batching
  6. Create client: Set up Apollo Client for frontend consumption

Best Practices#

  1. Use DataLoader - Always batch related queries to prevent N+1 problems
  2. Validate in resolvers - Check authentication and authorization
  3. Use GraphQL errors - Throw GraphQLError with appropriate codes
  4. Keep resolvers thin - Move business logic to services
  5. Schema-first development - Define schema before implementing resolvers
  6. Use fragments - Share field selections across queries
  7. Monitor performance - Track resolver execution times