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#
- Set up Apollo Server: Create the GraphQL endpoint with Next.js integration
- Define schema: Write your GraphQL type definitions
- Implement resolvers: Create resolver functions for queries and mutations
- Add context: Set up authentication and loaders in context
- Implement DataLoaders: Prevent N+1 queries with batching
- Create client: Set up Apollo Client for frontend consumption
Best Practices#
- Use DataLoader - Always batch related queries to prevent N+1 problems
- Validate in resolvers - Check authentication and authorization
- Use GraphQL errors - Throw GraphQLError with appropriate codes
- Keep resolvers thin - Move business logic to services
- Schema-first development - Define schema before implementing resolvers
- Use fragments - Share field selections across queries
- Monitor performance - Track resolver execution times
Related Patterns#
- Route Handler - REST API alternative
- Error Handling - Error response patterns
- React Query - Alternative to Apollo Client
- WebSockets - Real-time communication