Building a GraphQL API with Next.js and Apollo Server

Full Stack Engineer
January 3, 2025
Updated on March 10, 2025
0 MIN READ
#mobile-dev#typescript#cicd#security#building

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:

  1. Use a proper database instead of in-memory storage
  2. Implement authentication and authorization
  3. Add proper error handling
  4. 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:

  1. Unified API and frontend - Keep your API and frontend in the same project
  2. Type safety - GraphQL's strong typing helps catch errors early
  3. Efficient data fetching - Clients request exactly what they need
  4. 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.

Share this article