Skip to main content

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:

  1. Precise Data Fetching: Fetch only the data you need, reducing bandwidth usage
  2. Single Request: Get all required data in a single request, even from multiple sources
  3. Type Safety: Strong typing helps catch errors during development
  4. Real-time Updates: Support for subscriptions enables real-time features
  5. 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:

bash
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:

javascript
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:

javascript
// 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:

javascript
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:

bash
npm install @apollo/client

Set Up Apollo Client Provider

Create a new file called lib/apollo-provider.js:

javascript
'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:

javascript
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:

javascript
'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

javascript
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:

javascript
'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:

javascript
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

javascript
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

javascript
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:

javascript
'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

javascript
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

  1. Use Fragments for component-specific data requirements
  2. Implement Caching correctly to optimize performance
  3. Handle Loading and Error States for better user experience
  4. Use TypeScript with GraphQL code generation for type safety
  5. Consider Server Components when appropriate (fetch data on the server)
  6. Implement Authentication in your GraphQL API
  7. 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:

bash
npm install prisma @prisma/client
npx prisma init

Update your schema.prisma:

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:

bash
npx prisma migrate dev --name init

Update your resolvers to use Prisma:

javascript
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:

  1. Setting up a GraphQL server in Next.js
  2. Creating schemas and resolvers
  3. Querying data using Apollo Client
  4. Implementing mutations for data modification
  5. Building a complete Todo application
  6. Best practices for GraphQL in Next.js applications
  7. 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

Exercises

  1. Add pagination to the book list example
  2. Implement user authentication for the Todo application
  3. Create a comment system for the books using nested GraphQL queries
  4. Add filtering and sorting to the Todo application
  5. 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! :)