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.
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
, andbody
, all of which are of typeString
. These fields represent a post's data in the system.
2. Defining the Query Type
type Query
: TheQuery
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 ofPosts
. 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 theid
parameter (which is of typeInt
). It returns a singlePosts
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.
- This function handles the
-
singlePost
Resolver:- This function handles the
singlePost
query, which fetches a single post based on itsid
. - It calls
postService.getSinglePost()
with theid
as an argument and returns the corresponding post.
- This function handles the
In this case:
- The
posts
query in the schema is correctly matched by theposts
resolver. - The
singlePost
query in the schema is correctly matched by thesinglePost
resolver.