Building a GraphQL API with Next.js and Apollo Server
Introduction
GraphQL has become a popular alternative to REST APIs, offering more flexibility and efficiency in data fetching. When combined with Next.js for server-side rendering and Apollo Server for handling GraphQL operations, you can build powerful, performant APIs that seamlessly integrate with your frontend applications.
In this post, we'll walk through setting up a GraphQL API with Next.js and Apollo Server, covering schema definition, resolvers, and integration with Next.js API routes. Whether you're building a full-stack application or a headless CMS backend, this setup provides a solid foundation for your GraphQL needs.
Setting Up Apollo Server in Next.js
First, let's create a new Next.js project and install the necessary dependencies:
npx create-next-app@latest nextjs-graphql-api
cd nextjs-graphql-api
npm install @apollo/server graphql
Next, we'll set up Apollo Server in a new API route. Create a file at pages/api/graphql.js
:
import { ApolloServer } from '@apollo/server'; import { startServerAndCreateNextHandler } from '@as-integrations/next'; import { gql } from 'graphql'; const typeDefs = gql` type Query { hello: String } `; const resolvers = { Query: { hello: () => 'Hello from GraphQL!', }, }; const server = new ApolloServer({ typeDefs, resolvers, }); export default startServerAndCreateNextHandler(server);
This basic setup creates a GraphQL endpoint at /api/graphql
with a single query that returns a greeting string. The startServerAndCreateNextHandler
function from @as-integrations/next
bridges Apollo Server with Next.js API routes.
Defining Your GraphQL Schema
A well-structured schema is crucial for any GraphQL API. Let's expand our schema to include a more realistic data model for a blog application:
const typeDefs = gql` type Post { id: ID! title: String! content: String! author: User! createdAt: String! } type User { id: ID! name: String! email: String! posts: [Post!]! } type Query { posts: [Post!]! post(id: ID!): Post users: [User!]! user(id: ID!): User } type Mutation { createPost(title: String!, content: String!): Post! createUser(name: String!, email: String!): User! } `;
This schema defines:
- Post and User types with their fields
- Query operations for fetching data
- Mutation operations for creating new data
Implementing Resolvers
Resolvers are functions that handle the actual data fetching and manipulation for your GraphQL operations. Here's how we might implement resolvers for our blog schema:
// In-memory data store for demonstration const posts = []; const users = []; const resolvers = { Query: { posts: () => posts, post: (_, { id }) => posts.find(post => post.id === id), users: () => users, user: (_, { id }) => users.find(user => user.id === id), }, Mutation: { createPost: (_, { title, content }, context) => { // In a real app, you would validate the user is authenticated const post = { id: `post_${posts.length + 1}`, title, content, authorId: context.userId, // Would come from auth middleware createdAt: new Date().toISOString(), }; posts.push(post); return post; }, createUser: (_, { name, email }) => { const user = { id: `user_${users.length + 1}`, name, email, }; users.push(user); return user; }, }, Post: { author: (post) => users.find(user => user.id === post.authorId), }, User: { posts: (user) => posts.filter(post => post.authorId === user.id), }, };
Note that in a production application, you would:
- Use a proper database instead of in-memory storage
- Implement authentication and authorization
- Add proper error handling
- Consider data validation
Adding Authentication and Context
For most applications, you'll need to handle authentication. Here's how to modify our setup to include authentication middleware:
import { getToken } from 'next-auth/jwt'; const server = new ApolloServer({ typeDefs, resolvers, }); export default startServerAndCreateNextHandler(server, { context: async (req, res) => { // Get the session token const token = await getToken({ req }); return { req, res, userId: token?.sub, // Or whatever user identifier your auth system uses }; }, });
This example uses NextAuth.js, but you could adapt it for any authentication system. The context object is available in all your resolvers, allowing you to access the authenticated user's information.
Testing Your GraphQL API
With everything set up, you can test your API using GraphQL Playground or any GraphQL client. To access the playground, visit /api/graphql
in your browser when running the development server (npm run dev
).
Here's an example query you might run:
query GetPostsWithAuthors {
posts {
id
title
author {
name
email
}
}
}
And a mutation to create a new post:
mutation CreatePost {
createPost(title: "My First Post", content: "Hello GraphQL!") {
id
title
content
}
}
Conclusion
Building a GraphQL API with Next.js and Apollo Server provides a powerful combination for modern web development. This setup offers several advantages:
- Unified API and frontend - Keep your API and frontend in the same project
- Type safety - GraphQL's strong typing helps catch errors early
- Efficient data fetching - Clients request exactly what they need
- Real-time capabilities - Easy to add subscriptions for real-time updates
For production applications, you'll want to consider additional factors like:
- Database integration (Prisma, TypeORM, etc.)
- Performance optimization (DataLoader for batching and caching)
- Schema stitching for microservices
- Monitoring and logging
By starting with this foundation, you're well on your way to building flexible, efficient APIs that can evolve with your application's needs. The combination of Next.js and Apollo Server provides an excellent developer experience while delivering great performance for your users.