GraphQL with Next.js enables flexible, type-safe API development with powerful tooling.
Setup Apollo Server
npm install @apollo/server graphql graphql-tag
npm install -D @graphql-codegen/cli @graphql-codegen/typescript
API Route Handler
// app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { gql } from 'graphql-tag';
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
published: Boolean!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts(published: Boolean): [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String, authorId: ID!): Post!
publishPost(id: ID!): Post!
}
`;
const resolvers = {
Query: {
users: async () => {
return prisma.user.findMany();
},
user: async (_parent, { id }) => {
return prisma.user.findUnique({ where: { id } });
},
posts: async (_parent, { published }) => {
return prisma.post.findMany({
where: published !== undefined ? { published } : undefined,
});
},
post: async (_parent, { id }) => {
return prisma.post.findUnique({ where: { id } });
},
},
Mutation: {
createUser: async (_parent, { name, email }) => {
return prisma.user.create({
data: { name, email },
});
},
createPost: async (_parent, { title, content, authorId }) => {
return prisma.post.create({
data: {
title,
content,
authorId,
},
});
},
publishPost: async (_parent, { id }) => {
return prisma.post.update({
where: { id },
data: { published: true },
});
},
},
User: {
posts: async (parent) => {
return prisma.post.findMany({
where: { authorId: parent.id },
});
},
},
Post: {
author: async (parent) => {
return prisma.user.findUnique({
where: { id: parent.authorId },
});
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
const handler = startServerAndCreateNextHandler(server);
export { handler as GET, handler as POST };
Client Setup
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
export const client = new ApolloClient({
link: new HttpLink({
uri: '/api/graphql',
}),
cache: new InMemoryCache(),
});
// app/providers.tsx
'use client';
import { ApolloProvider } from '@apollo/client';
import { client } from '@/lib/apollo-client';
export function Providers({ children }: { children: React.ReactNode }) {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}
Queries in Components
'use client';
import { gql, useQuery } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
id
title
}
}
}
`;
export function UserList() {
const { data, loading, error } = useQuery(GET_USERS);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.users.map((user) => (
<li key={user.id}>
{user.name} ({user.posts.length} posts)
</li>
))}
</ul>
);
}
Mutations
import { gql, useMutation } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`;
export function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER, {
refetchQueries: ['GetUsers'],
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await createUser({
variables: {
name: formData.get('name'),
email: formData.get('email'),
},
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
Code Generation
# codegen.yml
schema: 'http://localhost:3000/api/graphql'
documents: 'src/**/*.tsx'
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
withComponent: false
npx graphql-codegen
// Using generated hooks
import { useGetUsersQuery, useCreateUserMutation } from '@/generated/graphql';
export function Users() {
const { data, loading } = useGetUsersQuery();
const [createUser] = useCreateUserMutation();
// Fully typed!
}
Pagination
const typeDefs = gql`
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PostEdge {
node: Post!
cursor: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Query {
posts(first: Int, after: String): PostConnection!
}
`;
const resolvers = {
Query: {
posts: async (_parent, { first = 10, after }) => {
const posts = await prisma.post.findMany({
take: first + 1,
skip: after ? 1 : 0,
cursor: after ? { id: after } : undefined,
orderBy: { createdAt: 'desc' },
});
const hasNextPage = posts.length > first;
const edges = posts.slice(0, first).map((post) => ({
node: post,
cursor: post.id,
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: await prisma.post.count(),
};
},
},
};
Error Handling
import { GraphQLError } from 'graphql';
const resolvers = {
Mutation: {
createUser: async (_parent, { email, name }) => {
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
throw new GraphQLError('User already exists', {
extensions: {
code: 'USER_ALREADY_EXISTS',
},
});
}
return prisma.user.create({ data: { email, name } });
},
},
};
DataLoader (N+1 Prevention)
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: [...userIds] } },
});
const userMap = new Map(users.map((user) => [user.id, user]));
return userIds.map((id) => userMap.get(id));
});
const resolvers = {
Post: {
author: async (parent, _args, { loaders }) => {
return loaders.user.load(parent.authorId);
},
},
};
GraphQL provides flexible, efficient APIs with strong typing and great developer experience.