Table of Contents


Build a CRUD GraphQL API with Next.js (App Router) - Without Apollo or Yoga

Introduction

When you think of APIs, REST usually comes to mind: multiple endpoints like /api/posts, /api/posts/:id, or /api/posts/:id/edit. But with GraphQL, you don’t need several endpoints - a single /graphql endpoint is enough, and the client specifies exactly what data it needs. This flexibility makes GraphQL a powerful alternative for modern applications. In this guide, we’ll walk through how to build a CRUD GraphQL API with Next.js App Router using only the official GraphQL package - no Apollo, no Yoga. You’ll learn how to set up a schema, create resolvers, and implement a clean /graphql route, along with a simple UI for creating, reading, updating, and deleting posts. If you’ve been searching for a straightforward Next.js GraphQL tutorial, this step-by-step walkthrough will give you everything you need to get started.

For example, instead of sending multiple REST requests, you can send one GraphQL query:


query { 
  posts { 
    id 
    title 
    content 
  } 
}					
					

The server returns exactly that data, nothing more, nothing less.

In this, we’ll build a CRUD (Create, Read, Update, Delete) GraphQL API inside a Next.js App Router. We'll use the official graphql package only - no Apollo, no Yoga. Data will be stored in memory (for demo purpose).

Step 1: Create a new Next.js app

We will use typescript here,


npx create-next-app@latest next-graphql-crud --typescript
cd next-graphql-crud						
						

Step 2: Install GraphQL

We only need one dependency: the official graphql library.


npm install graphql	
						

Step 3: Define the GraphQL Schema and Resolvers

Create a new folder called lib/ inside root directory, create a file called schema.ts.


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

// schema
export const schema = buildSchema(`
  type Post {
    id: ID!
    title: String!
    content: String!
  }

  type Query {
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createPost(title: String!, content: String!): Post!
    updatePost(id: ID!, title: String, content: String): Post!
    deletePost(id: ID!): Boolean!
  }
`);

type Post = { id: string; title: string; content: string };

const posts: Post[] = [];

// Resolvers
export const root = {
  posts: () => posts,
  post: ({ id }: { id: string }) => posts.find((p) => p.id === id),
  createPost: ({ title, content }: { title: string; content: string }) => {
    const newPost = { id: String(posts.length + 1), title, content };
    posts.push(newPost);
    return newPost;
  },
  updatePost: ({ id, title, content }: { id: string; title?: string; content?: string }) => {
    const post = posts.find((p) => p.id === id);
    if (!post) throw new Error("Post not found");
    if (title !== undefined) post.title = title;
    if (content !== undefined) post.content = content;
    return post;
  },
  deletePost: ({ id }: { id: string }) => {
    const index = posts.findIndex((p) => p.id === id);
    if (index === -1) return false;
    posts.splice(index, 1);
    return true;
  },
};
						

In above code we defined,

A Post type

Queries

Mutations (createPost, updatePost, deletePost)

Simple in-memory storage

Step 4: Create the GraphQL API route

Next.js App Router uses route handlers inside app/api/....

Create a file: app/api/graphql/route.ts.


// app/api/graphql/route.ts
import { schema, root } from "../../../../lib/schema";
import { graphql } from "graphql";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const { query, variables } = await req.json();

  const result = await graphql({
    schema,
    source: query,
    rootValue: root,
    variableValues: variables,
  });

  return NextResponse.json(result);
}
						

Our GraphQL endpoint is now available at:


http://localhost:3000/api/graphql
						

Step 5: Build the CRUD UI page

Add below code to app/page.tsx. This will include:

Fetch posts

Allow creating new posts

Edit existing posts

Delete posts


// app/page.tsx
"use client";
import { useState, useEffect } from "react";
type Post = { id: string; title: string; content: string };
export default function Home() {
 const [posts, setPosts] = useState<Post[]>([]);
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [editing, setEditing] = useState<Post | null>(null);

  // Fetch all posts
  async function fetchPosts() {
    const res = await fetch("/api/graphql", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        query: `query { posts { id title content } }`,
      }),
    });
    const json = await res.json();
    console.log("GraphQL response:", json);
    if (json.data?.posts) {
      setPosts(json.data.posts);
    } else {
      setPosts([]);
    }
  }
  // Create or Update post
  async function createOrUpdatePost() {
    const query = editing
      ? `mutation Update($id: ID!, $title: String, $content: String) {
          updatePost(id: $id, title: $title, content: $content) {
            id title content
          }
        }`
      : `mutation Create($title: String!, $content: String!) {
          createPost(title: $title, content: $content) {
            id title content
          }
        }`;
const variables = editing
      ? { id: editing.id, title, content }
      : { title, content };
await fetch("/api/graphql", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ query, variables }),
    });
 setTitle("");
    setContent("");
    setEditing(null);
    fetchPosts();
  }

  // Delete post
  async function deletePost(id: string) {
    await fetch("/api/graphql", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        query: `mutation Delete($id: ID!) { deletePost(id: $id) }`,
        variables: { id },
      }),
    });
    fetchPosts();
  }
  useEffect(() => {
    fetchPosts();
  }, []);

  return (
    <div className="p-6 space-y-4">
      <h1 className="text-2xl font-bold">Posts CRUD (GraphQL API)</h1>

      {/* Form */}
      <div className="space-y-2 w-[60%]">
        <div className="flex gap-2">
          <input
            className="border p-2 rounded w-1/2"
            placeholder="title"
            id="title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
          />
          <input
            className="border p-2 rounded w-1/2"
            placeholder="description"
            id="description"
            value={content}
            onChange={(e) => setContent(e.target.value)}
          />
        </div>

      </div>
        <button
          className="bg-blue-500 text-white px-4 py-2 rounded cursor-pointer"
          onClick={createOrUpdatePost}
        >
          {editing ? "Update Post" : "Create Post"}
        </button>
      {/* Posts List */}
      <div className="space-y-3 mt-4">
        {posts.map((post) => (
          <div
            key={post.id}
            className="border rounded p-3 flex justify-between items-center"
          >
            <div>
              <h2 className="font-semibold">{post.title}</h2>
              <p className="text-sm">{post.content}</p>
            </div>
            <div className="space-x-2">
              <button
                className="px-3 py-1 bg-yellow-400 rounded cursor-pointer"
                onClick={() => {
                  setEditing(post);
                  setTitle(post.title);
                  setContent(post.content);
                }}
              >
                Edit
              </button>
              <button
                className="px-3 py-1 bg-red-500 text-white rounded cursor-pointer"
                onClick={() => deletePost(post.id)}
              >
                Delete
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
						

Step 6: CRUD operations

Here’s what happens,

Create → mutation createPost(title, content)

Read → query posts

Update → mutation updatePost(id, title, content)

Delete → mutation deletePost(id)

All are handled by the single /api/graphql endpoint.

Now run below command in terminal,


npm run dev						
						

Here is how our UI will look,

Add new post

Update post

Delete Post

Need Expert Help with Next.js & GraphQL Development?

Building a scalable GraphQL API or a production-ready Next.js application can get tricky, especially when you need to handle advanced features like authentication, database integration, or performance optimization. That’s where Prishusoft comes in. With years of expertise in Next.js, GraphQL, and modern web development, our team delivers tailor-made solutions for startups, enterprises, and everything in between. Whether you’re looking to build a custom CRUD GraphQL API, migrate an existing REST API to GraphQL, or develop a full-featured Next.js app, we’ve got you covered.

Ready to bring your project to life? Contact Prishusoft today for professional development services that save you time, reduce costs, and accelerate your product launch.

Ready to Build Something Amazing?

Get in touch with Prishusoft – your trusted partner for custom software development. Whether you need a powerful web application or a sleek mobile app, our expert team is here to turn your ideas into reality.

image