English

GraphQL with Next.js enables flexible, type-safe API development with powerful tooling.

Setup Apollo Server

npm install @apollo/server graphql graphql-tag
npm install -D @graphql-codegen/cli @graphql-codegen/typescript

API Route Handler

// app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { gql } from 'graphql-tag';

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String
    published: Boolean!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts(published: Boolean): [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
    createPost(title: String!, content: String, authorId: ID!): Post!
    publishPost(id: ID!): Post!
  }
`;

const resolvers = {
  Query: {
    users: async () => {
      return prisma.user.findMany();
    },
    user: async (_parent, { id }) => {
      return prisma.user.findUnique({ where: { id } });
    },
    posts: async (_parent, { published }) => {
      return prisma.post.findMany({
        where: published !== undefined ? { published } : undefined,
      });
    },
    post: async (_parent, { id }) => {
      return prisma.post.findUnique({ where: { id } });
    },
  },

  Mutation: {
    createUser: async (_parent, { name, email }) => {
      return prisma.user.create({
        data: { name, email },
      });
    },
    createPost: async (_parent, { title, content, authorId }) => {
      return prisma.post.create({
        data: {
          title,
          content,
          authorId,
        },
      });
    },
    publishPost: async (_parent, { id }) => {
      return prisma.post.update({
        where: { id },
        data: { published: true },
      });
    },
  },

  User: {
    posts: async (parent) => {
      return prisma.post.findMany({
        where: { authorId: parent.id },
      });
    },
  },

  Post: {
    author: async (parent) => {
      return prisma.user.findUnique({
        where: { id: parent.authorId },
      });
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const handler = startServerAndCreateNextHandler(server);

export { handler as GET, handler as POST };

Client Setup

// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

export const client = new ApolloClient({
  link: new HttpLink({
    uri: '/api/graphql',
  }),
  cache: new InMemoryCache(),
});
// app/providers.tsx
'use client';

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

export function Providers({ children }: { children: React.ReactNode }) {
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

Queries in Components

'use client';

import { gql, useQuery } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

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

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.users.map((user) => (
        <li key={user.id}>
          {user.name} ({user.posts.length} posts)
        </li>
      ))}
    </ul>
  );
}

Mutations

import { gql, useMutation } from '@apollo/client';

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

export function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER, {
    refetchQueries: ['GetUsers'],
  });

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    await createUser({
      variables: {
        name: formData.get('name'),
        email: formData.get('email'),
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Code Generation

# codegen.yml
schema: 'http://localhost:3000/api/graphql'
documents: 'src/**/*.tsx'
generates:
  src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      withComponent: false
npx graphql-codegen
// Using generated hooks
import { useGetUsersQuery, useCreateUserMutation } from '@/generated/graphql';

export function Users() {
  const { data, loading } = useGetUsersQuery();
  const [createUser] = useCreateUserMutation();

  // Fully typed!
}

Pagination

const typeDefs = gql`
  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
  }

  type PostEdge {
    node: Post!
    cursor: String!
  }

  type PostConnection {
    edges: [PostEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
  }

  type Query {
    posts(first: Int, after: String): PostConnection!
  }
`;

const resolvers = {
  Query: {
    posts: async (_parent, { first = 10, after }) => {
      const posts = await prisma.post.findMany({
        take: first + 1,
        skip: after ? 1 : 0,
        cursor: after ? { id: after } : undefined,
        orderBy: { createdAt: 'desc' },
      });

      const hasNextPage = posts.length > first;
      const edges = posts.slice(0, first).map((post) => ({
        node: post,
        cursor: post.id,
      }));

      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor,
        },
        totalCount: await prisma.post.count(),
      };
    },
  },
};

Error Handling

import { GraphQLError } from 'graphql';

const resolvers = {
  Mutation: {
    createUser: async (_parent, { email, name }) => {
      const existing = await prisma.user.findUnique({ where: { email } });

      if (existing) {
        throw new GraphQLError('User already exists', {
          extensions: {
            code: 'USER_ALREADY_EXISTS',
          },
        });
      }

      return prisma.user.create({ data: { email, name } });
    },
  },
};

DataLoader (N+1 Prevention)

import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (userIds: readonly string[]) => {
  const users = await prisma.user.findMany({
    where: { id: { in: [...userIds] } },
  });

  const userMap = new Map(users.map((user) => [user.id, user]));
  return userIds.map((id) => userMap.get(id));
});

const resolvers = {
  Post: {
    author: async (parent, _args, { loaders }) => {
      return loaders.user.load(parent.authorId);
    },
  },
};

GraphQL provides flexible, efficient APIs with strong typing and great developer experience.

Posted onnextjswith tags:
0
0
0
0