Dec 18, 2024

How to Implement GraphQL in a Next.js App Router

I have pushed the entire project to my GitHub, which is discussed in today's posts.

Github: https://github.com/gitmahin/graphQL-with-nextjs-ssr.git

 

Today, we will learn how to implement GraphQL in our Next.js app router project. As I always prefer learning through projects, I’ll show you how to fetch posts using GraphQL.

But first, let’s understand what GraphQL is.

 

What is GraphQl and how it works?

GraphQL is a query language for APIs and a runtime for executing those queries against your data. It allows you to request exactly the data you need, making it more efficient than traditional APIs like REST, where fixed endpoints might return too much or too little information.

 

How GraphQL Works?

GraphQL Server:

  • The server has a schema that defines the types of data you can query and the relationships between those types.
  • It acts as an intermediary between the client and the actual data sources (e.g., databases, REST APIs, etc.).

Schema:
The schema is the blueprint of the data. It defines:

  • Types: The shape of the data (e.g., User, Post).
  • Queries: What data can be fetched (e.g., getUser, getPosts).
  • Mutations: How data can be modified (e.g., createPost, updateUser).
  • Resolvers: Functions that specify how to fetch or modify the data.

Client Requests:

  • The client sends a query to the server, asking for specific fields of data.
  • Example: Instead of getting all user data, the client may ask only for a user's name and email.

Server Execution:

  • The server uses resolvers to fetch the requested data from the appropriate sources.
  • The response is structured to match the query exactly.

Response:

  • The server sends the data back to the client in a predictable format, based on the query.

 

Now, let’s learn through a proper example

Suppose we have posts in our database. Each post can have several fields, but we only need specific ones: the thumbnail, title, meta description, slug, and published date. While we could fetch these posts using a REST API, the problem is that it would return unnecessary data, leading to inefficient queries. This is where GraphQL comes in — it allows us to retrieve exactly the data we need, without any excess.

So, let’s get started by creating a Next.js project in VS Code. Open your VS Code terminal and run the following command to create a new Next.js app:

npx create-next-app@latest

√ What is your project named? ... .
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like your code inside a `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to use Turbopack for `next dev`? ... No / Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No / Yes

 

After running the above commands and setting up your project, your Next.js app will be ready!

 

Setting Up a GraphQL Server

For this Next.js project, we will use the "graphql-yoga" GraphQL server library. It's lightweight and easy to set up, making it a great choice for serverless environments like Next.js. While you could also use Apollo Client for GraphQL, graphql-yoga works perfectly in this case as it integrates well with serverless functions.

Install graphql-yoga: Run the following command to install graphql-yoga and graphql as dependencies:

pnpm add graphql-yoga graphql

 

Now, let's take a look at our folder structure for GraphQl.

src/graphql

     |resolvers

         |---resolvers.ts

         |---post.resolver.ts

     |schemas

         |---schema.ts

         |---post.schema.ts

src/services

     |---post.service.ts

src/data/models

     |---postModel.ts

 

Now, let's start by creating the data models. I won’t go into detail about the postModel as it’s not part of the GraphQL tutorial.

Database setup

Install mongoose

pnpm i mongoose

 

Create Data Model

/data/models/postModel.ts

import mongoose, { Document, Schema } from "mongoose";

// Define the TypeScript interface for a PostType document.
// This ensures type safety and allows TypeScript to understand the structure of the Post model.
export interface PostType extends Document {
  userId: number; // The ID of the user associated with the post
  id: number; // Unique ID for the post
  title: string; // The title of the post
  body: string; // The content/body of the post
}

// Create a Mongoose schema that maps to the PostType interface.
// This schema defines the structure of the Post documents in the database.
const post_schema: Schema<PostType> = new Schema({
  userId: {
    type: Number, // Specifies that the `userId` field must be a number
  },
  id: {
    type: Number, // Specifies that the `id` field must be a number
  },
  title: {
    type: String, // Specifies that the `title` field must be a string
  },
  body: {
    type: String, // Specifies that the `body` field must be a string
  },
});

// Create the Mongoose model for the `Post` collection.
// If the model already exists in `mongoose.models`, reuse it; otherwise, create a new model.
const postModel =
  (mongoose.models.post as mongoose.Model<PostType>) || // Reuse existing model if available
  mongoose.model<PostType>("post", post_schema); // Create a new model with the schema

// Export the Post model for use in other parts of the application.
export default postModel;

 

Create a Separate Database Connection Function

/lib/connDb.ts

import mongoose from "mongoose";

// Define the ConnectionObject type to track the connection status.
// `isConnected` is optional and will be used to track whether the database is connected or not.
type ConnectionObject = {
  isConnected?: number; // Represents the connection state: 0 = disconnected, 1 = connected, 2 = connecting, 3 = disconnecting
};

// Create a connection object to store the connection status.
const connection: ConnectionObject = {};

// Async function to handle the database connection logic.
async function connDb(): Promise<void> {
  // Check if the connection is already established. If so, skip the connection process.
  if (connection.isConnected) {
    console.log("Database already connected");
    return; // Exit the function if already connected.
  }

  try {
    // Attempt to connect to MongoDB using the URI stored in the environment variable.
    const db = await mongoose.connect(process.env.MONGO_URI || "");

    // Store the connection status in the `connection` object.
    // `db.connections[0].readyState` gives the current connection state.
    connection.isConnected = db.connections[0].readyState;

    console.log("Database connected successfully");
  } catch (error) {
    // Log an error if the connection fails.
    console.log("Database connection failed");
    // Exit the process with an error code to indicate failure.
    process.exit(1);
  }
}

// Export the `connDb` function to be used in other parts of the application.
export default connDb;

 

Now, let's create a postService instance that contains the getAllPosts and getSinglePost methods. These methods will retrieve the post data from the database.

/services/posts.service.ts

import connDb from "@/lib/connDb";
import postModel from "@/data/models/postModel";

interface SinglePostType {
  id: number;
  skip?: number;
  limit?: number;
}

class PostService {
  constructor() {
    connDb();
  }

  async getAllPosts() {
    const posts = await postModel.find();
    return posts;
  }

  async getSinglePost({ id }: SinglePostType) {
    const post = await postModel.findOne({ id });
    return post;
  }
}

const postService = new PostService();
export default postService;

 

Create GraphQL Schemas & Resolvers

Finally, we’ve reached the point to create GraphQL Schemas & Resolvers.

First, let’s create our GraphQL schema. A GraphQL schema defines the structure of your data and the operations (queries or mutations) that clients can perform on it. It acts as a contract between the client and the server.

 

GraphQl Schema

As a best practice, we’ll modularize our schemas (e.g., post, user, etc.) by organizing them into separate files. These individual schema files will then be imported into a central schema.ts for a clean and maintainable structure.

  • schemas/post.schema.ts
  • schemas/schema.ts

schemas/post.schema.ts

const postDefs = `#graphql
  # Comments in GraphQL strings (such as this one) start with the hash (#) symbol.

  # This "Posts" type defines the queryable fields for every book in our data source.
  type Posts {
    userId: String
    id: String
    title: String
    body: String
  }

  # The "Query" type is special: it lists all of the available queries that
  # clients can execute, along with the return type for each. In this
  # case, the "posts" query returns an array of zero or more Posts (defined above).
  type Query {
    posts: [Posts],
    singlePost(id: Int): Posts
  }
`;

export default postDefs;

This code defines a GraphQL schema for handling "posts". Let’s break it down step by step:

 

1. Defining the Posts Type

  • type Posts: This defines the structure of a post.
  • The Posts type has four fields: userId, id, title, and body, all of which are of type String. These fields represent a post's data in the system.

 

2. Defining the Query Type

  • type Query: The Query type is a special type in GraphQL that defines all the available read operations (queries) that a client can execute.
  • posts: This query returns an array of Posts. The square brackets ([]) indicate that the result will be a list of posts.
  • singlePost(id: Int): Posts: This query fetches a single post based on the id parameter (which is of type Int). It returns a single Posts object.

 

3. GraphQL Schema Comments

# Comments in GraphQL strings (such as this one) start with the hash (#) symbol.

 

Import all schemas into the main schema.ts file.

// Import the GraphQL schema definition for the Post type.
import postDefs from "./post.schema";

// Export the consolidated type definitions, including `postDefs`.
export const typeDefs = `#graphql
  ${postDefs}
  #...more defs
`;

 

GraphQl Resolver

A resolver is a function that handles fetching data for a specific query or mutation in GraphQL. In this case, the resolver is for the Query type.

/resolvers/post.resolver.ts

import postService from "@/services/posts.service";

// Define the structure of arguments passed to the resolvers.
interface ArgsType {
  page?: number;
}

interface SinglePostType {
  id: number;
}

// Resolver logic for the GraphQL `Query` type.
const postsResolver = {
  Query: {
    // Fetch all posts with optional pagination logic.
    posts: async (_: unknown, { page }: ArgsType) => {
      // Example pagination logic (can be implemented as needed):
      // const limit = 6;
      // const skip = (page - 1) * limit;

      const data = await postService.getAllPosts();
      return data;
    },

    // Fetch a single post based on its id. cause slug is not in data
    singlePost: async (_: unknown, { id }: SinglePostType) => {
      const data = await postService.getSinglePost({ id });
      return data;
    },
  },
};

export default postsResolver;

 

  • posts Resolver:

    • This function handles the posts query, which fetches all posts.
    • The page argument (optional) could be used for pagination, but pagination is commented out in this example.
    • It calls postService.getAllPosts() to fetch all posts from the backend and returns the data.
  • singlePost Resolver:

    • This function handles the singlePost query, which fetches a single post based on its id.
    • It calls postService.getSinglePost() with the id as an argument and returns the corresponding post.

In this case:

  • The posts query in the schema is correctly matched by the posts resolver.
  • The singlePost query in the schema is correctly matched by the singlePost resolver.

 

 

The names defined in the Query type of the schema and the corresponding Query resolvers must match exactly. If they don't, an error will be thrown.

"To simplify this process and avoid such issues, there’s an excellent library called Nexus. In a future tutorial, we’ll dive into Nexus, a powerful library for building GraphQL schemas in a code-first approach. Nexus allows developers to define GraphQL types, queries, and mutations directly in TypeScript or JavaScript, providing strong type safety and an excellent developer experience.

With Nexus, you don’t have to write GraphQL schema definitions as strings (#graphql). Instead, you define them programmatically using Nexus’s intuitive syntax. This not only eliminates the need for separate schema files but also enables features like autocomplete, error checking, and inline documentation in your editor."

 

Combine multiple resolvers into one object

// Import resolver logic for `post`-related queries.
import postsResolver from "./post.resolver";

// Combine all resolvers into a single object for the GraphQL server.
const resolvers = {
    Query: {
        ...postsResolver.Query, // Include `postsResolver` Query fields.
    },
};

export default resolvers;
  • Here, you're creating a new resolvers object that consolidates all resolver functions for the Query type.

  • ...postsResolver.Query: This spreads the Query fields from the postsResolver into the new resolvers.Query object. It means you're essentially adding all the queries (such as posts and singlePost) from the postsResolver into the Query section of the main resolvers object.

 

Why Use the Spread Operator (...)?

The spread operator (...) allows you to combine the fields from postsResolver.Query into the Query object of the main resolvers. If you had other resolvers (e.g., for user, comment, etc.), you would also use a similar approach to add them to the resolvers object.

 

Almost There! Let's Create the /api/graphql Route to Serve the GraphQL Endpoint

/app/api/graphql/route.ts

// Next.js Custom Route Handler: https://nextjs.org/docs/app/building-your-application/routing/router-handlers
import { createSchema, createYoga } from "graphql-yoga";
import { typeDefs } from "@/graphql/schemas/schema";
import resolvers from "@/graphql/resolvers/resolvers";

interface NextContext {
  params: Promise<Record<string, string>>;
}

const { handleRequest } = createYoga<NextContext>({
  schema: createSchema({
    typeDefs,
    resolvers,
  }),

  // While using Next.js file convention for routing, we need to configure Yoga to use the correct endpoint
  graphqlEndpoint: "/api/graphql",

  // Yoga needs to know how to create a valid Next response
  fetchAPI: {
    Request,
    Response,
  },
});

export {
  handleRequest as GET,
  handleRequest as POST,
  handleRequest as OPTIONS,
};

Let's break down this code step by step:

1. createSchema & createYoga Function

  • createSchema: This function from the graphql-yoga library is used to create a GraphQL schema by combining your typeDefs (schema definition) and resolvers.
  • createYoga: This function is used to set up a GraphQL server using graphql-yoga. It handles the server-side logic for processing GraphQL queries and mutations.

 

2. NextContext

NextContext: This is a TypeScript interface that defines the shape of the context that will be passed to the Yoga GraphQL handler. In this case, params represents dynamic URL parameters, which are part of the request context.

 

3. Creating the Yoga GraphQL Handler

createYoga: This function is called to set up the GraphQL Yoga server. It receives an object with several configurations:

  • schema: The schema is created using createSchema(), where typeDefs and resolvers are passed in.
  • graphqlEndpoint: This specifies the endpoint where the GraphQL API will be available. In this case, it’s set to /api/graphql, so the server will handle requests to /api/graphql.
  • fetchAPI: This specifies how Yoga will interact with the Request and Response classes provided by Next.js. It ensures that Yoga is aware of how to handle requests and responses in a Next.js environment.

 

4. Exporting the Request Handlers

  • Here, you're exporting the handleRequest function from the Yoga instance as three HTTP methods: GET, POST, and OPTIONS. These are the HTTP methods that the /api/graphql route will handle:
    • GET: Handles incoming GraphQL queries.
    • POST: Handles GraphQL mutations.
    • OPTIONS: Handles pre-flight requests (for CORS or other cross-origin requests).

This structure allows your Next.js app to act as a GraphQL server, handling requests to the /api/graphql endpoint.

 

Now that everything is set up, you can navigate to http://localhost:3000/api/graphql in your browser. The GraphQL Yoga UI will appear, allowing you to interact with your defined types and resolvers by executing queries and mutations directly.

 

Fetching GraphQL Data in the Frontend with Next.js SSR

Prerequisites

  • A Next.js app with a GraphQL API running on /api/graphql (as we set up previously).
  • Basic understanding of how GraphQL works (queries and mutations).
  • graphql and @apollo/client libraries installed for querying the GraphQL server.

 

Step 1: Install Apollo Client

To interact with a GraphQL API on the frontend, we’ll use Apollo Client, which is a popular JavaScript library for managing GraphQL data. It provides a simple API for fetching, caching, and managing data.

First, you need to install the required libraries:

pnpm install @apollo/client graphql

 

Step 2: Set Up Apollo Client

Create an Apollo Client instance that will allow us to interact with our GraphQL server. We’ll define the client configuration in a separate file for better organization.

  • Create a file called apolloClient.ts in the lib folder:
import { ApolloClient, InMemoryCache } from "@apollo/client";

// Create an instance of ApolloClient with GraphQL API endpoint and caching strategy.
const apolloClient = new ApolloClient({
  uri: "http://localhost:3000/api/graphql", // The GraphQL server endpoint (relative path for server-side rendering compatibility).
  cache: new InMemoryCache(), // In-memory cache for efficient query data management.
});

export default apolloClient;
  • HttpLink: This defines the URL to your GraphQL endpoint.
  • InMemoryCache: Apollo uses this to cache the data fetched from GraphQL for better performance.

 

Step 3: Wrap the Whole App with ApolloProvider

To set up Apollo Client in your Next.js app, we need to wrap the entire app with the ApolloProvider. This makes Apollo Client available throughout the app, allowing you to interact with GraphQL data easily.

Create: /app/components/providers/apollo-client-provider.tsx

"use client"; // Enables client-side rendering for this component in Next.js.

import React from "react";
import { ApolloProvider } from "@apollo/client"; // ApolloProvider integrates Apollo Client with the React app.
import apolloClient from "@/lib/apollo"; // Import the configured Apollo Client instance.

export default function ApolloClientProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  // Wraps the React app with ApolloProvider to enable GraphQL operations via Apollo Client.
  return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>;
}

 

Modify the layout.tsx

<html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {/* Wraps the app in the Apollo Client provider for GraphQL integration. */}
        <ApolloClientProvider>{children}</ApolloClientProvider>
      </body>
</html>

 

Step 4: Fetch Data with Apollo Client In Next.Js Server Side Component

import React from "react"; // Importing React library for JSX syntax
import BlogClientComponent from "./blog-client-comp"; // Importing the client-side component that will display the blog posts
import apolloClient from "@/lib/apollo"; // Importing the configured Apollo client instance for GraphQL queries
import { gql } from "@apollo/client"; // Importing gql template literal to define GraphQL queries

// Defining the shape of a single Post object
interface PostType {
  userId: number;
  id: number;
  title: string;
  body: string;
}

// GraphQL query to fetch posts, requesting only necessary fields
const GET_POSTS = gql`
  query {
    posts {
      userId
      id
      title
    }
  }
`;

// Defining the structure of the expected response for posts from the GraphQL query
type QueryType = {
  posts: PostType[];
};

// The main BlogPage component that fetches and displays posts
export default async function BlogPage() {
  // Fetching the posts from the GraphQL API, with 'no-cache' fetch policy to ensure fresh data every time
  const { data } = await apolloClient.query<QueryType>({
    query: GET_POSTS, // The query being executed
  });

  return <BlogClientComponent data={data.posts} />;
}

Let's break down this code step by step:

1. Import Statements

  • BlogClientComponent: A child component that handles the rendering of the fetched posts.
  • apolloClient: The configured Apollo Client instance for making GraphQL queries.
  • gql: A template literal provided by Apollo Client to define GraphQL queries.

 

2. Post Data Shape (TypeScript Interfaces)

  • PostTypeDefines the structure of a single post object
  • QueryTypeDefines the structure of the response returned by the GraphQL query

 

3. Defining the GraphQL Query

// GraphQL query to fetch posts, requesting only necessary fields
const GET_POSTS = gql`
  query {
    posts {
      userId
      id
      title
    }
  }
`;

This is a GraphQL query using the gql template literal. It specifies:

  • The query name: posts
  • The fields to fetch: userId, id, and title.

 

4. Fetching Data

  // Fetching the posts from the GraphQL API, with 'no-cache' fetch policy to ensure fresh data every time
  const { data } = await apolloClient.query<QueryType>({
    query: GET_POSTS, // The query being executed
  });
  • query: Executes the GraphQL query.
  • QueryType: Ensures the response matches the expected data structure.
  • { data }: Destructures the data field from the query's result.
  • No Cache: This query does not specify a cache policy but will fetch fresh data by default. You can add options like fetchPolicy: "no-cache" to force skipping the cache.

 

5. Passing Data to the Client Component

The fetched posts (data.posts) are passed to the client-side component (BlogClientComponent) as a prop:

return <BlogClientComponent data={data.posts} />

This separates concerns:

  • The server-side component (BlogPage) handles data fetching.
  • The client-side component (BlogClientComponent) handles rendering.

 

Advantages of This Setup

  1. Type Safety: Using TypeScript ensures the data structure matches what the GraphQL API returns.
  2. Optimized Performance: Fetching data on the server reduces client-side load and improves SEO.
  3. Modularity: Separating fetching logic (BlogPage) and rendering logic (BlogClientComponent) makes the code cleaner and easier to maintain.