Setting Up a GraphQL Server and Client in Next.js: A Comprehensive Guide for Modern Web Development

  • by
  • 10 min read

In the ever-evolving landscape of web development, the combination of GraphQL and Next.js has emerged as a powerful duo for building efficient, scalable, and flexible applications. This comprehensive guide will walk you through the process of setting up both a GraphQL server and client within a Next.js application, providing you with the knowledge and tools to leverage these technologies effectively.

Understanding the GraphQL and Next.js Synergy

GraphQL, developed by Facebook in 2012 and open-sourced in 2015, has revolutionized API development by offering unparalleled flexibility and efficiency. Unlike traditional REST APIs, GraphQL allows clients to request exactly the data they need, nothing more and nothing less. This approach significantly reduces over-fetching of data and enables the retrieval of multiple resources in a single request, leading to improved performance and a better developer experience.

Next.js, on the other hand, is a React framework that has gained immense popularity since its initial release in 2016. It provides a robust set of features including server-side rendering, static site generation, and API routes, making it an ideal choice for building modern web applications. The framework's simplicity and performance have made it a go-to solution for developers seeking to create fast, SEO-friendly React applications.

When we combine GraphQL with Next.js, we create a synergy that addresses many of the challenges faced in modern web development. The declarative nature of GraphQL complements Next.js's component-based architecture, allowing for more efficient data fetching and management. This integration enables developers to build highly performant applications that provide a seamless experience for both users and developers alike.

Setting Up the Project: A Detailed Walkthrough

Let's begin by creating a new Next.js project and installing the necessary dependencies. We'll use pnpm as our package manager for this guide, but you can use npm or yarn if you prefer.

First, create a new Next.js project by running the following command in your terminal:

pnpm create next-app

This command will prompt you to name your project and select your preferred options. Once the project is created, navigate to the project directory and install the required dependencies:

pnpm install @apollo/server graphql @as-integrations/next apollo-server-core @apollo/client graphql-tag @nextui-org/react

These packages include Apollo Server for creating our GraphQL server, Apollo Client for the frontend, and NextUI for building our user interface.

Next, let's set up our project structure. Create the following directory structure in your src folder:

src/
├── graphql/
│   ├── queries/
│   │   ├── getUsers.gql
│   │   └── searchUser.gql
│   ├── apollo-client.js
│   ├── resolvers.js
│   └── schemas.js
├── pages/
│   ├── api/
│   │   └── graphql.js
│   ├── _app.js
│   ├── _document.js
│   └── index.js
└── utils/
    └── cors.js

This structure separates our GraphQL-related files, pages, and utility functions, promoting a clean and organized codebase.

To complete our setup, create a .env.local file in the root directory of your project and add the following environment variables:

NEXT_PUBLIC_URL_SERVER_GRAPHQL=http://localhost:3000/api/graphql
URL_API=https://dummyjson.com/users

These environment variables will be used to configure our GraphQL server and client.

Crafting the GraphQL Server: A Deep Dive

Now that we have our project structure in place, let's dive into setting up our GraphQL server. We'll start by creating the server in the /api/graphql.js file.

In this file, we'll use Apollo Server, a popular GraphQL server that integrates well with Next.js. Here's the code for our server:

import { ApolloServer } from "@apollo/server";
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
import typeDefs from "@/graphql/schemas";
import resolvers from "@/graphql/resolvers";
import allowCors from "@/utils/cors";

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginLandingPageGraphQLPlayground()]
});

const handler = startServerAndCreateNextHandler(apolloServer, {
  context: async (req, res) => ({ req, res }),
});

export default allowCors(handler);

This code creates an instance of ApolloServer with our schema (typeDefs) and resolvers. We're also including the GraphQL Playground plugin, which provides an interactive environment for testing our GraphQL queries.

Next, let's define our GraphQL schema in graphql/schemas.js:

import { gql } from 'graphql-tag';

const typeDefs = gql`
  type Query {
    users: [User]
    searchUser(value: String): [User]
  }

  type User {
    id: ID
    firstName: String
    lastName: String
    email: String
    username: String
    image: String
  }
`;

export default typeDefs;

This schema defines two queries: users to fetch all users, and searchUser to search for users based on a given value. We also define the User type with fields for id, firstName, lastName, email, username, and image.

Now, let's implement the resolvers for these queries in graphql/resolvers.js:

const resolvers = {
  Query: {
    users: async () => {
      try {
        const response = await fetch(process.env.URL_API);
        const data = await response.json();
        return data.users.map(u => ({
          id: u.id,
          firstName: u.firstName,
          lastName: u.lastName,
          email: u.email,
          username: u.username,
          image: u.image
        }));
      } catch (error) {
        throw new Error("Something went wrong");
      }
    },
    searchUser: async (_, { value }) => {
      try {
        const response = await fetch(`${process.env.URL_API}/search?q=${value}`);
        const data = await response.json();
        return data.users.map(u => ({
          id: u.id,
          firstName: u.firstName,
          lastName: u.lastName,
          email: u.email,
          username: u.username,
          image: u.image
        }));
      } catch (error) {
        throw new Error("Something went wrong");
      }
    }
  }
};

export default resolvers;

These resolvers implement the logic for our queries, fetching data from the dummy JSON API we specified in our environment variables.

To handle CORS (Cross-Origin Resource Sharing) issues, we'll create a utility function in utils/cors.js:

const allowCors = (fn) => async (req, res) => {
  res.setHeader('Access-Control-Allow-Credentials', true);
  res.setHeader('origin', 'https://nextjs-graphql-server-client.vercel.app');
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT');
  res.setHeader(
    'Access-Control-Allow-Headers',
    'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
  );
  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }
  await fn(req, res);
};

export default allowCors;

This function sets the necessary headers to allow cross-origin requests, which is crucial when your frontend and backend are hosted on different domains.

Configuring the GraphQL Client: Bridging the Gap

With our server set up, let's move on to configuring the GraphQL client. We'll use Apollo Client, a comprehensive state management library for JavaScript that enables us to manage both local and remote data with GraphQL.

First, let's create an instance of Apollo Client in graphql/apollo-client.js:

import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
  uri: process.env.NEXT_PUBLIC_URL_SERVER_GRAPHQL,
  cache: new InMemoryCache(),
});

export default client;

This code creates an Apollo Client instance, pointing it to our GraphQL server and setting up an in-memory cache for optimal performance.

Next, we need to wrap our Next.js application with the ApolloProvider. In pages/_app.js, add the following code:

import { ApolloProvider } from '@apollo/client';
import client from "@/graphql/apollo-client";

export default function App({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

This ensures that all components in our application have access to the Apollo Client.

Now, let's create our GraphQL queries. In graphql/queries/getUsers.gql, add:

query getUsers {
  users {
    id
    firstName
    lastName
    email
    username
    image
  }
}

And in graphql/queries/searchUsers.gql, add:

query getSearchUsers($value: String) {
  searchUser(value: $value) {
    id
    firstName
    lastName
    email
    username
    image
  }
}

These queries define the data we want to retrieve from our GraphQL server.

To use these .gql files in our Next.js application, we need to configure Webpack. In next.config.js, add the following configuration:

const nextConfig = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.(graphql|gql)/,
      exclude: /node_modules/,
      loader: "graphql-tag/loader"
    });
    return config;
  }
};

module.exports = nextConfig;

This configuration allows us to import our .gql files directly in our JavaScript code.

Building the User Interface: Bringing it All Together

Now that we have both our server and client set up, let's build the user interface. We'll create a simple interface that displays a list of users and allows searching for users.

In pages/index.js, implement the main page of your application:

import { useEffect, useRef, useState } from 'react';
import { useLazyQuery, useQuery } from '@apollo/client';
import Head from 'next/head';
import { Button, Container, Grid, Input, Spacer, User, Row, Loading } from "@nextui-org/react";
import GET_USERS from '@/graphql/queries/getUsers.gql';
import SEARCH_USERS from '@/graphql/queries/searchUsers.gql';

export default function Home() {
  const [users, setUsers] = useState([]);
  const [searchValue, setSearchValue] = useState('');
  const usersRef = useRef(null);

  const { data, loading, error } = useQuery(GET_USERS);

  const [getSearchedUsers] = useLazyQuery(SEARCH_USERS, {
    fetchPolicy: 'network-only',
    onCompleted(data) {
      setUsers(data.searchUser);
    }
  });

  useEffect(() => {
    if (data) {
      setUsers(data.users);
      usersRef.current = data.users;
    }
  }, [data]);

  const searchUser = () => {
    getSearchedUsers({
      variables: {
        value: searchValue
      }
    });
  };

  if (error) {
    console.error(error);
    return null;
  }

  return (
    <>
      <Head>
        <title>Next.js and GraphQL Setup</title>
        <meta name="description" content="Next.js and GraphQL integration example" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <Container css={{ display: 'flex', justifyContent: 'center' }}>
          <Spacer y={2.5} />
          <Row justify="center" align="center">
            <Input
              clearable
              labelPlaceholder="User"
              onClearClick={() => setUsers(usersRef.current)}
              initialValue={searchValue}
              onChange={(e) => setSearchValue(e.target.value)}
            />
            <Button color="gradient" auto onClick={searchUser}>
              Search user
            </Button>
          </Row>
          <Spacer y={2.5} />
          <Row justify="center" align="center">
            {loading ? (
              <Loading />
            ) : (
              <Grid.Container gap={2} justify="center">
                {users.map(u => (
                  <Grid xs={3} key={u.id}>
                    <User
                      src={u.image}
                      name={`${u.firstName} ${u.lastName}`}
                      description={u.email}
                      size="lg"
                      bordered
                      color="gradient"
                    />
                  </Grid>
                ))}
              </Grid.Container>
            )}
          </Row>
        </Container>
      </main>
    </>
  );
}

This code creates a user interface with a search input and a grid of user cards. It uses the useQuery hook to fetch all users when the component mounts, and the useLazyQuery hook to search for users when the search button is clicked.

Advanced Concepts and Best Practices

While the setup we've walked through provides a solid foundation, there are several advanced concepts and best practices to consider as you continue to develop your application:

  1. Authentication and Authorization: Implement a robust authentication system to secure your GraphQL API. You can use JSON Web Tokens (JWT) or OAuth 2.0 for authentication, and implement field-level authorization in your resolvers.

  2. Error Handling: Develop a comprehensive error handling strategy. Apollo Server allows you to customize error messages and add additional metadata to errors, which can be invaluable for debugging and improving user experience.

  3. Caching: Leverage Apollo Client's caching capabilities to improve performance. You can customize the cache behavior for different queries and implement cache invalidation strategies to ensure data consistency.

  4. Optimistic UI Updates: Implement optimistic UI updates for mutations to provide a more responsive user experience. This involves updating the UI immediately after a mutation is initiated, before receiving a response from the server.

  5. Pagination: For large datasets, implement pagination in your GraphQL API. This can be done using the offset-limit approach or with cursor-based pagination for better performance with large datasets.

  6. Real-time Updates: Consider implementing subscriptions for real-time updates. While not covered in this guide, GraphQL subscriptions can be a powerful tool for building real-time features in your application.

  7. Code Splitting: Utilize Next.js's built-in code splitting capabilities to optimize your application's load time. You can use dynamic imports to lazy-load components and Apollo Client's @client directive for local state management.

  8. Testing: Implement a comprehensive testing strategy. This should include unit tests for your resolvers, integration tests for your GraphQL API, and end-to-end tests for your Next.js application.

  9. Performance Monitoring: Use tools like Apollo Studio to monitor the performance of your GraphQL API. This can help you identify slow queries and optimize your schema and resolvers.

  10. Schema Design: As your application grows, pay close attention to your schema design. Use interfaces and unions to create more flexible and reusable types, and consider implementing a federated GraphQL architecture for large-scale applications.

Conclusion: Embracing the Future of Web Development

By following this comprehensive guide, you've not only set up a GraphQL server and client in a Next.js application but also gained insights into the powerful synergy between these technologies. This combination provides a solid foundation for building modern, efficient, and scalable web applications.

Remember that the world of web development is constantly evolving, and staying updated with the latest trends and best practices is crucial. Continue to explore the ecosystem around GraphQL and Next.js, participate in community discussions, and contribute to open-source projects to deepen your understanding and skills.

As you move forward with your development journey,

Did you like this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.