Next.js GraphQL Integration
Introduction
GraphQL is a query language for APIs that provides a more efficient, powerful, and flexible alternative to traditional REST. When combined with Next.js, it creates a powerful stack for building modern web applications with efficient data fetching capabilities.
In this guide, we'll explore how to integrate GraphQL with your Next.js application. You'll learn how to set up a GraphQL server, create schemas, and query data from your Next.js frontend. This integration allows for type-safe APIs and more precise data fetching, reducing over-fetching issues common with REST APIs.
Why GraphQL with Next.js?
Before diving into the implementation, let's understand why GraphQL is a great fit for Next.js applications:
- Precise Data Fetching: Fetch only the data you need, reducing bandwidth usage
- Single Request: Get all required data in a single request, even from multiple sources
- Type Safety: Strong typing helps catch errors during development
- Real-time Updates: Support for subscriptions enables real-time features
- Great Developer Experience: Excellent tooling and self-documenting APIs
Setting Up GraphQL in Next.js
Let's start by setting up a basic GraphQL server in a Next.js application using Apollo Server.
Step 1: Install Required Packages
First, install the necessary dependencies:
npm install @apollo/server graphql @as-integrations/next cors
Step 2: Create a GraphQL Schema
Create a new file called schema.js
in your project:
export const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
publishYear: Int
genre: String
}
type Query {
books: [Book]
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!, publishYear: Int, genre: String): Book
}
`;
Step 3: Create Resolvers
Next, create a resolvers.js
file:
// Sample data
let books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', publishYear: 1925, genre: 'Classic' },
{ id: '2', title: '1984', author: 'George Orwell', publishYear: 1949, genre: 'Dystopian' },
{ id: '3', title: 'To Kill a Mockingbird', author: 'Harper Lee', publishYear: 1960, genre: 'Fiction' },
];
export const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find(book => book.id === id),
},
Mutation: {
addBook: (_, { title, author, publishYear, genre }) => {
const newBook = {
id: String(books.length + 1),
title,
author,
publishYear,
genre,
};
books.push(newBook);
return newBook;
},
},
};
Step 4: Create the GraphQL API Route
In Next.js 13+, create a route handler in the app/api/graphql/route.js
file:
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { typeDefs } from '@/schema';
import { resolvers } from '@/resolvers';
const server = new ApolloServer({
typeDefs,
resolvers,
});
const handler = startServerAndCreateNextHandler(server);
export { handler as GET, handler as POST };
Querying the GraphQL API
Now that we have our GraphQL server set up, let's see how we can query it from our Next.js frontend.
Using Apollo Client
First, install Apollo Client:
npm install @apollo/client
Set Up Apollo Client Provider
Create a new file called lib/apollo-provider.js
:
'use client';
import { ApolloClient, InMemoryCache, ApolloProvider, HttpLink } from '@apollo/client';
export function ApolloWrapper({ children }) {
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: '/api/graphql',
}),
});
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}
Wrap Your App with Apollo Provider
Update your app/layout.js
file:
import { ApolloWrapper } from '@/lib/apollo-provider';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ApolloWrapper>{children}</ApolloWrapper>
</body>
</html>
);
}
Create a Client Component to Query Data
Create a component called BookList.js
:
'use client';
import { gql, useQuery } from '@apollo/client';
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author
genre
}
}
`;
export default function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Book List</h2>
<ul>
{data.books.map(book => (
<li key={book.id}>
<strong>{book.title}</strong> by {book.author} ({book.genre})
</li>
))}
</ul>
</div>
);
}
Use the Component in Your Page
import BookList from '@/components/BookList';
export default function Home() {
return (
<main>
<h1>My Book Library</h1>
<BookList />
</main>
);
}
Mutations with GraphQL
Mutations allow you to modify server-side data. Let's implement a form to add new books.
Create a component called AddBook.js
:
'use client';
import { gql, useMutation } from '@apollo/client';
import { useState } from 'react';
const ADD_BOOK = gql`
mutation AddBook($title: String!, $author: String!, $publishYear: Int, $genre: String) {
addBook(title: $title, author: $author, publishYear: $publishYear, genre: $genre) {
id
title
author
}
}
`;
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author
genre
}
}
`;
export default function AddBook() {
const [formState, setFormState] = useState({
title: '',
author: '',
publishYear: '',
genre: ''
});
const [addBook] = useMutation(ADD_BOOK, {
refetchQueries: [{ query: GET_BOOKS }]
});
const handleSubmit = async (e) => {
e.preventDefault();
const publishYear = formState.publishYear ? parseInt(formState.publishYear) : null;
await addBook({
variables: {
...formState,
publishYear
}
});
setFormState({
title: '',
author: '',
publishYear: '',
genre: ''
});
};
return (
<div>
<h2>Add New Book</h2>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title:</label>
<input
id="title"
value={formState.title}
onChange={(e) => setFormState({...formState, title: e.target.value})}
required
/>
</div>
<div>
<label htmlFor="author">Author:</label>
<input
id="author"
value={formState.author}
onChange={(e) => setFormState({...formState, author: e.target.value})}
required
/>
</div>
<div>
<label htmlFor="publishYear">Year Published:</label>
<input
id="publishYear"
type="number"
value={formState.publishYear}
onChange={(e) => setFormState({...formState, publishYear: e.target.value})}
/>
</div>
<div>
<label htmlFor="genre">Genre:</label>
<input
id="genre"
value={formState.genre}
onChange={(e) => setFormState({...formState, genre: e.target.value})}
/>
</div>
<button type="submit">Add Book</button>
</form>
</div>
);
}
Add this component to your page:
import BookList from '@/components/BookList';
import AddBook from '@/components/AddBook';
export default function Home() {
return (
<main>
<h1>My Book Library</h1>
<AddBook />
<BookList />
</main>
);
}
Real-World Example: Building a Todo App
Let's create a more complete example of a Todo application with GraphQL and Next.js.
1. Update Schema
export const typeDefs = `#graphql
type Todo {
id: ID!
text: String!
completed: Boolean!
createdAt: String!
}
type Query {
todos: [Todo]
todo(id: ID!): Todo
}
type Mutation {
addTodo(text: String!): Todo
toggleTodo(id: ID!): Todo
deleteTodo(id: ID!): ID
}
`;
2. Update Resolvers
let todos = [
{ id: '1', text: 'Learn GraphQL', completed: false, createdAt: new Date().toISOString() },
{ id: '2', text: 'Build a Next.js app', completed: false, createdAt: new Date().toISOString() },
];
export const resolvers = {
Query: {
todos: () => todos,
todo: (_, { id }) => todos.find(todo => todo.id === id),
},
Mutation: {
addTodo: (_, { text }) => {
const newTodo = {
id: String(todos.length + 1),
text,
completed: false,
createdAt: new Date().toISOString(),
};
todos.push(newTodo);
return newTodo;
},
toggleTodo: (_, { id }) => {
const todo = todos.find(todo => todo.id === id);
if (!todo) {
throw new Error('Todo not found');
}
todo.completed = !todo.completed;
return todo;
},
deleteTodo: (_, { id }) => {
todos = todos.filter(todo => todo.id !== id);
return id;
},
},
};
3. Create Todo Components
TodoList.js:
'use client';
import { gql, useQuery, useMutation } from '@apollo/client';
import { useState } from 'react';
const GET_TODOS = gql`
query GetTodos {
todos {
id
text
completed
createdAt
}
}
`;
const ADD_TODO = gql`
mutation AddTodo($text: String!) {
addTodo(text: $text) {
id
text
completed
createdAt
}
}
`;
const TOGGLE_TODO = gql`
mutation ToggleTodo($id: ID!) {
toggleTodo(id: $id) {
id
completed
}
}
`;
const DELETE_TODO = gql`
mutation DeleteTodo($id: ID!) {
deleteTodo(id: $id)
}
`;
export default function TodoList() {
const [newTodo, setNewTodo] = useState('');
const { loading, error, data } = useQuery(GET_TODOS);
const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data: { addTodo } }) {
const { todos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { todos: [...todos, addTodo] },
});
}
});
const [toggleTodo] = useMutation(TOGGLE_TODO);
const [deleteTodo] = useMutation(DELETE_TODO, {
update(cache, { data: { deleteTodo } }) {
const { todos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { todos: todos.filter(todo => todo.id !== deleteTodo) },
});
}
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
const handleAddTodo = (e) => {
e.preventDefault();
if (!newTodo.trim()) return;
addTodo({ variables: { text: newTodo } });
setNewTodo('');
};
const handleToggleTodo = (id) => {
toggleTodo({ variables: { id } });
};
const handleDeleteTodo = (id) => {
deleteTodo({ variables: { id } });
};
return (
<div className="todo-app">
<h2>Todo List</h2>
<form onSubmit={handleAddTodo} className="add-todo-form">
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new task..."
/>
<button type="submit">Add</button>
</form>
<ul className="todo-list">
{data.todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => handleDeleteTodo(todo.id)} className="delete-btn">
Delete
</button>
</li>
))}
</ul>
</div>
);
}
4. Use the Todo List in Your Page
import TodoList from '@/components/TodoList';
export default function TodoApp() {
return (
<main>
<h1>GraphQL Todo App</h1>
<TodoList />
</main>
);
}
Best Practices for Next.js and GraphQL Integration
- Use Fragments for component-specific data requirements
- Implement Caching correctly to optimize performance
- Handle Loading and Error States for better user experience
- Use TypeScript with GraphQL code generation for type safety
- Consider Server Components when appropriate (fetch data on the server)
- Implement Authentication in your GraphQL API
- Use Batching and Deduplication to reduce network requests
Persisting Data
In a real-world application, you would connect your GraphQL server to a database. Here's a quick example using Prisma with SQLite:
First, install Prisma:
npm install prisma @prisma/client
npx prisma init
Update your schema.prisma:
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model Todo {
id String @id @default(uuid())
text String
completed Boolean @default(false)
createdAt DateTime @default(now())
}
Run the migrations:
npx prisma migrate dev --name init
Update your resolvers to use Prisma:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const resolvers = {
Query: {
todos: async () => {
return await prisma.todo.findMany();
},
todo: async (_, { id }) => {
return await prisma.todo.findUnique({ where: { id } });
},
},
Mutation: {
addTodo: async (_, { text }) => {
return await prisma.todo.create({
data: { text },
});
},
toggleTodo: async (_, { id }) => {
const todo = await prisma.todo.findUnique({ where: { id } });
return await prisma.todo.update({
where: { id },
data: { completed: !todo.completed },
});
},
deleteTodo: async (_, { id }) => {
await prisma.todo.delete({ where: { id } });
return id;
},
},
};
Summary
In this guide, we've covered:
- Setting up a GraphQL server in Next.js
- Creating schemas and resolvers
- Querying data using Apollo Client
- Implementing mutations for data modification
- Building a complete Todo application
- Best practices for GraphQL in Next.js applications
- Data persistence with Prisma
GraphQL offers an excellent alternative to traditional REST APIs, providing a more flexible and efficient way to fetch and manipulate data in your Next.js applications. The combination of Next.js and GraphQL creates a powerful stack for building modern web applications with efficient data handling.
Additional Resources
- Apollo Client Documentation
- GraphQL Official Documentation
- Next.js Documentation
- Prisma Documentation
Exercises
- Add pagination to the book list example
- Implement user authentication for the Todo application
- Create a comment system for the books using nested GraphQL queries
- Add filtering and sorting to the Todo application
- Implement real-time updates using GraphQL subscriptions
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)