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.