Skip to main content
Back to Blog
Tutorials
3 min read
December 12, 2024

How to Build a GraphQL API in Next.js with Yoga and Pothos

Set up a type-safe GraphQL API inside your Next.js app using GraphQL Yoga for the server and Pothos for the schema builder.

Ryel Banfield

Founder & Lead Developer

GraphQL gives clients the power to request exactly the data they need. Here is how to set up a production-ready GraphQL API inside a Next.js app.

Install Dependencies

pnpm add graphql graphql-yoga @pothos/core @pothos/plugin-prisma
pnpm add -D prisma

Define Your Pothos Schema Builder

// lib/graphql/builder.ts
import SchemaBuilder from "@pothos/core";
import PrismaPlugin from "@pothos/plugin-prisma";
import type PrismaTypes from "@pothos/plugin-prisma/generated";
import { prisma } from "@/lib/prisma";

export const builder = new SchemaBuilder<{
  PrismaTypes: PrismaTypes;
  Scalars: {
    DateTime: {
      Input: Date;
      Output: Date;
    };
  };
}>({
  plugins: [PrismaPlugin],
  prisma: {
    client: prisma,
    filterConnectionTotalCount: true,
  },
});

// Register DateTime scalar
builder.scalarType("DateTime", {
  serialize: (val) => val.toISOString(),
  parseValue: (val) => new Date(val as string),
});

// Initialize query and mutation types
builder.queryType({});
builder.mutationType({});

Define Object Types

// lib/graphql/types/user.ts
import { builder } from "../builder";

builder.prismaObject("User", {
  fields: (t) => ({
    id: t.exposeID("id"),
    name: t.exposeString("name"),
    email: t.exposeString("email"),
    avatar: t.exposeString("avatar", { nullable: true }),
    createdAt: t.expose("createdAt", { type: "DateTime" }),
    posts: t.relation("posts", {
      query: { orderBy: { createdAt: "desc" } },
    }),
    postCount: t.relationCount("posts"),
  }),
});
// lib/graphql/types/post.ts
import { builder } from "../builder";

const PostStatus = builder.enumType("PostStatus", {
  values: ["DRAFT", "PUBLISHED", "ARCHIVED"] as const,
});

builder.prismaObject("Post", {
  fields: (t) => ({
    id: t.exposeID("id"),
    title: t.exposeString("title"),
    slug: t.exposeString("slug"),
    content: t.exposeString("content"),
    status: t.expose("status", { type: PostStatus }),
    publishedAt: t.expose("publishedAt", { type: "DateTime", nullable: true }),
    author: t.relation("author"),
    tags: t.relation("tags"),
  }),
});

Define Queries

// lib/graphql/queries/posts.ts
import { builder } from "../builder";
import { prisma } from "@/lib/prisma";

builder.queryField("posts", (t) =>
  t.prismaField({
    type: ["Post"],
    args: {
      status: t.arg({ type: "PostStatus", required: false }),
      limit: t.arg.int({ defaultValue: 20 }),
      offset: t.arg.int({ defaultValue: 0 }),
      search: t.arg.string({ required: false }),
    },
    resolve: async (query, _parent, args) => {
      return prisma.post.findMany({
        ...query,
        where: {
          ...(args.status && { status: args.status }),
          ...(args.search && {
            OR: [
              { title: { contains: args.search, mode: "insensitive" } },
              { content: { contains: args.search, mode: "insensitive" } },
            ],
          }),
        },
        take: Math.min(args.limit ?? 20, 100),
        skip: args.offset ?? 0,
        orderBy: { createdAt: "desc" },
      });
    },
  })
);

builder.queryField("post", (t) =>
  t.prismaField({
    type: "Post",
    nullable: true,
    args: {
      slug: t.arg.string({ required: true }),
    },
    resolve: (query, _parent, args) => {
      return prisma.post.findUnique({
        ...query,
        where: { slug: args.slug },
      });
    },
  })
);

Define Mutations

// lib/graphql/mutations/posts.ts
import { builder } from "../builder";
import { prisma } from "@/lib/prisma";

const CreatePostInput = builder.inputType("CreatePostInput", {
  fields: (t) => ({
    title: t.string({ required: true }),
    slug: t.string({ required: true }),
    content: t.string({ required: true }),
    authorId: t.string({ required: true }),
    tagIds: t.stringList({ required: false }),
  }),
});

builder.mutationField("createPost", (t) =>
  t.prismaField({
    type: "Post",
    args: {
      input: t.arg({ type: CreatePostInput, required: true }),
    },
    resolve: async (query, _parent, { input }) => {
      return prisma.post.create({
        ...query,
        data: {
          title: input.title,
          slug: input.slug,
          content: input.content,
          status: "DRAFT",
          author: { connect: { id: input.authorId } },
          ...(input.tagIds && {
            tags: {
              connect: input.tagIds.map((id) => ({ id })),
            },
          }),
        },
      });
    },
  })
);

builder.mutationField("publishPost", (t) =>
  t.prismaField({
    type: "Post",
    args: { id: t.arg.string({ required: true }) },
    resolve: (query, _parent, { id }) => {
      return prisma.post.update({
        ...query,
        where: { id },
        data: { status: "PUBLISHED", publishedAt: new Date() },
      });
    },
  })
);

Build the Schema

// lib/graphql/schema.ts
import { builder } from "./builder";

// Import all type definitions (side-effect imports)
import "./types/user";
import "./types/post";
import "./queries/posts";
import "./mutations/posts";

export const schema = builder.toSchema();

Create the Route Handler

// app/api/graphql/route.ts
import { createYoga } from "graphql-yoga";
import { schema } from "@/lib/graphql/schema";

const { handleRequest } = createYoga({
  schema,
  graphqlEndpoint: "/api/graphql",
  fetchAPI: {
    Request: globalThis.Request,
    Response: globalThis.Response,
  },
});

export { handleRequest as GET, handleRequest as POST };

This gives you a GraphQL playground at /api/graphql in development.

Client-Side Usage

"use client";

import { useEffect, useState } from "react";

const POSTS_QUERY = `
  query Posts($status: PostStatus, $limit: Int) {
    posts(status: $status, limit: $limit) {
      id
      title
      slug
      status
      publishedAt
      author {
        name
      }
    }
  }
`;

export function PostsList() {
  const [posts, setPosts] = useState<any[]>([]);

  useEffect(() => {
    fetch("/api/graphql", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        query: POSTS_QUERY,
        variables: { status: "PUBLISHED", limit: 10 },
      }),
    })
      .then((res) => res.json())
      .then((data) => setPosts(data.data.posts));
  }, []);

  return (
    <ul className="space-y-2">
      {posts.map((post) => (
        <li key={post.id} className="border rounded p-3">
          <a href={`/blog/${post.slug}`} className="font-medium hover:underline">
            {post.title}
          </a>
          <span className="text-sm text-muted-foreground ml-2">
            by {post.author.name}
          </span>
        </li>
      ))}
    </ul>
  );
}

Need a Custom API?

We design and build GraphQL and REST APIs for businesses of all sizes. Contact us to discuss your project.

GraphQLAPINext.jsYogaPothostutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles