Building a GraphQL Server with Next.js API Routes

Create a powerful, flexible API within your Next.js application. Learn schema design, resolver implementation, and Apollo Client integration for modern web development.

Why GraphQL with Next.js?

Modern web development demands flexible, efficient data fetching solutions. GraphQL has emerged as a powerful alternative to traditional REST APIs, offering developers precise control over the data they request. When combined with Next.js, which provides built-in API routes, you get a seamless development experience that lets you build both your frontend and backend within a single framework.

Key benefits of this approach:

  • Single endpoint - One GraphQL endpoint handles all data requirements
  • Zero configuration - Next.js API routes require minimal setup
  • Type-safe schemas - Self-documenting API contracts
  • Flexible queries - Clients request exactly the data they need

This guide covers everything from basic setup to production-ready implementation. For teams looking to modernize their web development practices, GraphQL with Next.js provides an excellent foundation for scalable API architecture.

What You'll Learn

API Route Setup

Create GraphQL endpoints in Next.js using both Pages Router and App Router approaches

Schema Design

Define types, queries, and mutations using GraphQL Schema Definition Language

Resolver Implementation

Connect your schema to databases and external APIs with efficient data fetching

Apollo Client

Integrate a powerful GraphQL client with caching, optimistic updates, and error handling

Performance Optimization

Implement query depth limiting, response caching, and request batching

Security Best Practices

Add authentication, input validation, and rate limiting to protect your API

Setting Up Your Next.js GraphQL Server

Creating a GraphQL server within Next.js requires only a few dependencies and a single API route file. Whether you're using the Pages Router or the modern App Router, the core pattern remains straightforward: define your schema, implement resolvers, and handle incoming requests.

If you're new to Next.js development, our comprehensive Getting Started with Next.js guide provides additional context for setting up your development environment before diving into GraphQL implementation.

Install GraphQL dependencies
1npm install graphql2 3# For Apollo Server integration (optional)4npm install @apollo/server graphql-tag5 6# For GraphQL Yoga (modern alternative)7npm install graphql-yoga

Your First GraphQL Endpoint

Here's a minimal implementation using Next.js API routes. This example demonstrates the essential components: schema definition, resolver functions, and request handling.

Basic GraphQL server in Next.js API routes
1import { graphql, buildSchema } from 'graphql';2 3const schema = buildSchema(`4 type Query {5 hello: String6 greeting(name: String!): String7 users: [User!]!8 }9 10 type User {11 id: ID!12 name: String!13 email: String!14 }15`);16 17const users = [18 { id: '1', name: 'Alice', email: '[email protected]' },19 { id: '2', name: 'Bob', email: '[email protected]' },20];21 22const rootValue = {23 hello: () => 'Hello, World!',24 greeting: ({ name }) => `Hello, ${name}!`,25 users: () => users,26};27 28export default async function handler(req, res) {29 const { query, variables } = req.body;30 31 const response = await graphql({32 schema,33 source: query,34 rootValue,35 variableValues: variables,36 });37 38 return res.status(200).json(response);39}

Defining Your GraphQL Schema

The schema is the foundation of any GraphQL API. It defines your data types, available operations, and the relationships between entities. A well-designed schema serves as both documentation and contract between backend and frontend teams.

Object Types and Relationships

GraphQL schemas use object types to represent the main entities in your application. Each object type contains fields with specific types, supporting both simple values and nested relationships. When designing your schema, think about how data entities relate to each other--this is similar to planning your database structure in a full-stack composable architecture.

GraphQL schema with types, queries, and mutations
1type User {2 id: ID!3 name: String!4 email: String!5 posts: [Post!]!6}7 8type Post {9 id: ID!10 title: String!11 content: String!12 author: User!13 createdAt: String!14}15 16type Query {17 users: [User!]!18 user(id: ID!): User19 postsByAuthor(authorId: ID!): [Post!]!20 latestPosts(limit: Int = 10): [Post!]!21}22 23type Mutation {24 createUser(input: CreateUserInput!): User!25 updateUser(id: ID!, input: UpdateUserInput!): User!26 deleteUser(id: ID!): Boolean!27}28 29input CreateUserInput {30 name: String!31 email: String!32}

Implementing Resolver Functions

Resolvers are the functions that fetch the data described by your schema. Each field in your schema maps to a resolver function that returns the actual data. Resolvers can fetch from databases, call external APIs, or combine multiple data sources.

Database-Connected Resolvers

In production applications, resolvers typically interact with databases. Understanding how databases work at a conceptual level helps when designing efficient resolvers--our database concepts for frontend developers guide provides essential background.

Database-connected resolver functions
1import { Pool } from 'pg';2 3const pool = new Pool({4 connectionString: process.env.DATABASE_URL,5});6 7const rootValue = {8 users: async () => {9 const result = await pool.query('SELECT * FROM users');10 return result.rows;11 },12 13 user: async ({ id }) => {14 const result = await pool.query(15 'SELECT * FROM users WHERE id = $1',16 [id]17 );18 return result.rows[0] || null;19 },20 21 postsByAuthor: async ({ authorId }) => {22 const result = await pool.query(23 `SELECT * FROM posts 24 WHERE author_id = $1 25 ORDER BY created_at DESC`,26 [authorId]27 );28 return result.rows;29 },30 31 createUser: async ({ input }) => {32 const result = await pool.query(33 `INSERT INTO users (name, email) 34 VALUES ($1, $2) 35 RETURNING *`,36 [input.name, input.email]37 );38 return result.rows[0];39 },40};

Apollo Client Integration

Apollo Client provides a powerful solution for consuming GraphQL APIs in React applications. It handles caching automatically, tracks loading and error states, and integrates seamlessly with Next.js server-side rendering.

Setting Up Apollo Client

Apollo Client configuration
1// lib/apolloClient.js2import { ApolloClient, InMemoryCache } from '@apollo/client';3 4const client = new ApolloClient({5 uri: '/api/graphql',6 cache: new InMemoryCache(),7});8 9export default client;

Fetching Data with useQuery

The useQuery hook executes GraphQL queries and provides loading, error, and data states:

Using useQuery hook for data fetching
1import { useQuery, gql } from '@apollo/client';2 3const GET_USERS = gql`4 query GetUsers {5 users {6 id7 name8 email9 }10 }11`;12 13function UsersList() {14 const { loading, error, data } = useQuery(GET_USERS);15 16 if (loading) return <p>Loading users...</p>;17 if (error) return <p>Error: {error.message}</p>;18 19 return (20 <ul>21 {data.users.map(user => (22 <li key={user.id}>23 {user.name} ({user.email})24 </li>25 ))}26 </ul>27 );28}

Modifying Data with useMutation

Mutations use the useMutation hook, which returns a trigger function that executes when users take action:

Using useMutation hook for data modification
1import { useMutation, gql } from '@apollo/client';2 3const CREATE_USER = gql`4 mutation CreateUser($input: CreateUserInput!) {5 createUser(input: $input) {6 id7 name8 email9 }10 }11`;12 13function CreateUserForm() {14 const [createUser, { loading }] = useMutation(CREATE_USER);15 16 const handleSubmit = async (event) => {17 event.preventDefault();18 const formData = new FormData(event.target);19 20 await createUser({21 variables: {22 input: {23 name: formData.get('name'),24 email: formData.get('email'),25 },26 },27 refetchQueries: [{ query: GET_USERS }],28 });29 30 event.target.reset();31 };32 33 return (34 <form onSubmit={handleSubmit}>35 <input name="name" placeholder="Name" required />36 <input name="email" type="email" placeholder="Email" required />37 <button type="submit" disabled={loading}>38 {loading ? 'Creating...' : 'Create User'}39 </button>40 </form>41 );42}

Performance Optimization

GraphQL's flexibility can lead to performance challenges without proper safeguards. Implementing query depth limiting, caching, and batching protects your server while maintaining excellent response times.

Query Depth Limiting

Prevent complex nested queries from overwhelming your server:

Query depth limiting implementation
1const depthLimit = (maxDepth) => ({2 Field: {3 enter(node, _key, _parent) {4 const depth = getDepth(node);5 if (depth > maxDepth) {6 throw new Error(7 `Query depth (${depth}) exceeds maximum (${maxDepth})`8 );9 }10 },11 },12});13 14function getDepth(node, depth = 0) {15 if (!node.selectionSet) return depth;16 const childDepths = node.selectionSet.selections.map(17 child => getDepth(child, depth + 1)18 );19 return Math.max(...childDepths, depth);20}21 22// Apply depth limit to your GraphQL execution23export default async function handler(req, res) {24 const response = await graphql({25 schema,26 source: req.body.query,27 rootValue,28 extensions: {29 depthLimit: depthLimit(10),30 },31 });32 33 return res.status(200).json(response);34}

Response Caching

Cache GraphQL responses to reduce database load and improve response times:

GraphQL response caching
1// Using Next.js built-in caching2export default async function handler(req, res) {3 const cacheKey = `graphql:${req.body.query}`;4 5 // Check cache first6 const cached = await cache.get(cacheKey);7 if (cached) {8 return res.status(200).json(JSON.parse(cached));9 }10 11 const response = await graphql({12 schema,13 source: req.body.query,14 rootValue,15 });16 17 // Cache the response for 60 seconds18 await cache.set(cacheKey, JSON.stringify(response), 'EX', 60);19 20 return res.status(200).json(response);21}

Error Handling and Security

Robust error handling distinguishes between development and production responses, while security measures protect against abuse.

Custom Error Formatting

Implement different error responses based on error type:

Custom error handling for GraphQL
1export default async function handler(req, res) {2 try {3 const response = await graphql({4 schema,5 source: req.body.query,6 rootValue,7 errors: {8 formatError: (error) => {9 // Hide internal errors in production10 if (process.env.NODE_ENV === 'production') {11 if (error.originalError?.message) {12 return { message: 'Internal server error' };13 }14 }15 return {16 message: error.message,17 locations: error.locations,18 path: error.path,19 };20 },21 },22 });23 24 return res.status(200).json(response);25 } catch (error) {26 console.error('GraphQL error:', error);27 return res.status(500).json({28 errors: [{ message: 'Internal server error' }],29 });30 }31}

Authentication Context

Implement authentication by attaching user information to the GraphQL context:

Authentication context in GraphQL
1export default async function handler(req, res) {2 // Extract and verify token3 const token = req.headers.authorization?.split(' ')[1];4 const user = token ? await verifyToken(token) : null;5 6 const response = await graphql({7 schema,8 source: req.body.query,9 rootValue,10 contextValue: { user, req, res },11 });12 13 return res.status(200).json(response);14}15 16// In resolvers, check authentication17const rootValue = {18 // Public field19 users: () => users,20 21 // Protected field22 userEmail: ({ id }, context) => {23 if (!context.user) {24 throw new Error('Authentication required');25 }26 const user = users.find(u => u.id === id);27 return user?.email;28 },29};

Conclusion

Building a GraphQL server with Next.js API routes provides a powerful, flexible approach to API development. The combination of GraphQL's precise data fetching with Next.js's developer-friendly deployment creates an excellent foundation for modern web applications.

Key takeaways:

  • Simple setup - Create GraphQL endpoints with minimal code
  • Flexible schema - Define types that precisely match your domain
  • Efficient data fetching - Resolvers connect to any data source
  • Optimized performance - Implement caching, depth limiting, and batching
  • Secure APIs - Add authentication, error handling, and rate limiting

This approach scales gracefully as your application grows while remaining maintainable and type-safe. Ready to implement GraphQL in your Next.js project? Our web development team has extensive experience building scalable APIs and modern web applications that leverage these technologies effectively.


Sources:

Frequently Asked Questions

What's the difference between Pages Router and App Router for GraphQL?

Both routers support GraphQL equally well. The Pages Router uses the familiar pages/api directory with export default handler functions. The App Router uses route.js files with GET and POST export functions. The core GraphQL implementation remains the same--only the request handling syntax differs.

Do I need Apollo Server or can I use the graphql library directly?

The core graphql library provides all essential functionality for schema definition and query execution. Apollo Server adds features like GraphiQL, persisted queries, and response caching. For simple use cases, the graphql library is sufficient. For production applications with advanced requirements, Apollo Server is recommended.

How does GraphQL caching compare to REST API caching?

GraphQL caching operates at the query level rather than the URL level. Apollo Client handles automatic normalized caching on the client side. On the server, you can cache entire query responses using keys derived from the query string and variables. GraphQL's flexibility means cache invalidation requires more thought than REST's simple URL-based caching.

Can I use GraphQL with Next.js server-side rendering?

Yes, Apollo Client integrates with Next.js SSR through the getStaticProps and getServerSideProps methods, or through React Server Components in the App Router. You can prefetch data on the server and include it in the initial HTML render, improving both performance and SEO.

Ready to Build Your Next.js Application?

Our team specializes in modern web development using Next.js, GraphQL, and cutting-edge technologies. Let's discuss how we can help bring your project to life.