Next.js 和 GraphQL:全栈开发的完美组合

2025-06-08

Next.js 和 GraphQL:全栈开发的完美组合

您将学到什么

在今天的文章中,我们将使用 Next.js 和 GraphQL Yoga 创建一个全栈应用程序。

最终结果

本文涵盖了哪些内容

  • Next.js 应用路由器和操作
  • GraphQL Yoga 集成
  • 对数据库执行获取、创建和删除等操作

先决条件

在开始本文之前,建议您了解 React、Next.js 和 GraphQL。

创建项目

要在 Remix 中初始化项目,我们执行以下命令:

npx create-next-app@latest my-app
Enter fullscreen mode Exit fullscreen mode

所使用的设置包括 TypeScript、ESLint、Tailwind CSS,并且我们正在使用app路由器。

我们使用以下命令启动开发服务器:

npm run dev
Enter fullscreen mode Exit fullscreen mode

除了基本配置之外,我们还使用daisyUI库来使用预先设计的组件。

npm install daisyui
Enter fullscreen mode Exit fullscreen mode

然后我们将库添加到tailwind.config.js文件中的插件列表中,我们还可以定义我们想要使用的主题,如下所示:

module.exports = {
  // ...
  plugins: [require("daisyui")],
  daisyui: {
    themes: ["winter"],
  },
};
Enter fullscreen mode Exit fullscreen mode

应用程序设置完成后,我们可以继续下一步。

后端设置

首先,我们需要配置与数据库的连接,以便将数据持久化到应用程序中。为了简化整个过程,我们将使用 ORM,在本文中我选择了Drizzle ORM。至于数据库,我选择使用 SQLite,因为它最易于访问。

我们首先安装依赖项:

npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
Enter fullscreen mode Exit fullscreen mode

然后在server/文件夹内我们将创建一个名为的文件夹db/,其中包含连接和数据库模式。

从配置开始,让我们server/db/config.ts使用以下内容创建文件:

import { drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import Database from "better-sqlite3";

const sqlite = new Database("sqlite.db");

export const db = drizzle(sqlite);

migrate(db, { migrationsFolder: "./server/db/migrations" });
Enter fullscreen mode Exit fullscreen mode

下一步是创建数据库模式,在今天的文章中,我们将在数据库中只有一个表,其中todos包含三列,如下所示:

import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const todos = sqliteTable("todos", {
  id: integer("id").primaryKey(),
  title: text("username").notNull(),
  createdAt: integer("createdAt").notNull(),
});
Enter fullscreen mode Exit fullscreen mode

上述代码位于server/db/schema.ts文件中,该文件将被考虑用于创建数据库迁移和实体。现在package.json让我们添加以下脚本:

{
  // ...
  "scripts": {
    // ...
    "db:migrations": "drizzle-kit generate:sqlite --out ./server/db/migrations --schema ./server/db/schema.ts"
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

从上面的脚本中,我们可以运行以下命令来创建迁移,同时考虑到已创建的模式:

npm run db:migrations
Enter fullscreen mode Exit fullscreen mode

一旦完成,预计migrations/将在文件夹内创建文件夹server/db/

一旦我们准备好数据层,我们就可以开始处理我们的 GraphQL 层,首先安装以下依赖项:

# graphql related dependencies
npm install garph graphql-yoga graphql
# JS Dates
npm install dayjs
Enter fullscreen mode Exit fullscreen mode

下一步无疑是使用Garph创建我们的 GraphQL Schema ,以创建一个完全类型安全的 API,而无需进行代码生成。

我们的应用程序架构将只有一个查询 (Query),它将负责返回todos数据库中的所有内容。此外,我们还将有两个变更 (Mutations),一个用于插入一个todo,另一个用于删除一个现有的todo

为此,让我们创建一个名为的文件夹,gql/其中server/将包含与我们的模式相关的所有内容,可能类似于以下内容:

import { GarphSchema } from "garph";

export const g = new GarphSchema();

export const TodoGQL = g.type("Todo", {
  id: g.int(),
  title: g.string(),
  createdAt: g.int(),
});

export const queryType = g.type("Query", {
  getTodos: g.ref(TodoGQL).list().description("Gets an array of todos"),
});

export const mutationType = g.type("Mutation", {
  addTodo: g
    .ref(TodoGQL)
    .args({
      title: g.string(),
    })
    .description("Adds a new todo"),
  removeTodo: g
    .ref(TodoGQL)
    .optional()
    .args({
      id: g.int(),
    })
    .description("Removes an existing todo"),
});
Enter fullscreen mode Exit fullscreen mode

有了文件中创建的模式schema.ts,我们现在可以创建resolvers.ts包含查询逻辑和 API 的每个变更的 . 代码如下:

import { InferResolvers } from "garph";
import { YogaInitialContext } from "graphql-yoga";
import { eq } from "drizzle-orm";
import dayjs from "dayjs";

import { mutationType, queryType } from "./schema";
import { db } from "../db/config";
import { todos } from "../db/schema";

type Resolvers = InferResolvers<
  { Query: typeof queryType; Mutation: typeof mutationType },
  { context: YogaInitialContext }
>;

export const resolvers: Resolvers = {
  Query: {
    getTodos: (_, __, ctx) => {
      return db.select().from(todos).all();
    },
  },
  Mutation: {
    addTodo: (_, { title }, ctx) => {
      return db
        .insert(todos)
        .values({
          title,
          createdAt: dayjs().unix(),
        })
        .returning()
        .get();
    },
    removeTodo: (_, { id }, ctx) => {
      return db.delete(todos).where(eq(todos.id, id)).returning().get();
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

创建模式和解析器后,我们现在需要创建index.ts构建 GraphQL 模式的文件,以便 GraphQL Yoga 可以使用它。

import { buildSchema } from "garph";

import { resolvers } from "./resolvers";
import { g } from "./schema";

export const schema = buildSchema({ g, resolvers });
Enter fullscreen mode Exit fullscreen mode

这样,一切就绪,我们现在可以跳转到router app创建以下文件夹结构的位置,app/api/graphql/其中route.ts包含包含以下内容的文件:

import { createYoga } from "graphql-yoga";

import { schema } from "../../../server/gql";

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

export { handleRequest as GET, handleRequest as POST }
Enter fullscreen mode Exit fullscreen mode

在上面的代码块中,我们创建了一个自定义路由处理程序,它将创建一个GraphQL Yoga实例,该实例将根据刚刚创建的模式和解析器提供我们的 API。

后端完成后,我们可以继续下一步。

可重用组件

在开始页面设计之前,我们先来了解一下页面中需要用到的一些组件。这些组件与列表、列表内渲染的元素(行)以及一些操作相关。

首先安装以下依赖项:

npm install graphql-request zod
Enter fullscreen mode Exit fullscreen mode

接下来,让我们创建一个<ListItem />组件,该组件将与列表中的每一行相对应,并将在应用的根目录中渲染。该组件将接收一些 props,例如todoIdtitle以及一个removeItem()函数。为了实现交互,该组件将在客户端渲染。

"use client";

import { LiHTMLAttributes } from "react";

interface Props extends LiHTMLAttributes<HTMLLIElement> {
  todoId: number;
  title: string;
  removeItem: (id: number) => void;
}

export default function ListItem({ todoId, title, removeItem, ...rest }: Props) {
  return (
    <li
      className="card w-96 bg-base-100 shadow-xl cursor-pointer"
      {...rest}
      onClick={() => removeItem(todoId)}
    >
      <div className="card-body">
        <p>{title}</p>
      </div>
    </li>
  );
}
Enter fullscreen mode Exit fullscreen mode

然后我们可以创建一些将在应用程序列表中使用的操作,我们只需要确保这些函数在调用时始终在服务器端运行。

"use server";

import { revalidatePath } from "next/cache";
import { GraphQLClient, gql } from "graphql-request";

const mutation = gql`
  mutation removeTodo($id: Int!) {
    removeTodo(id: $id) {
      id
    }
  }
`;

export async function removeTodo(id: number) {
  const graphQLClient = new GraphQLClient("http://localhost:3000/api/graphql");
  await graphQLClient.request(mutation, { id });
  revalidatePath("/");
}
Enter fullscreen mode Exit fullscreen mode

上述代码是在app/components/文件夹内创建的,更具体地说,是在list/包含文件中函数的文件夹中创建的actions.ts,并且文件中将包含组件代码index.tsx。该组件仍需创建,并将在服务器端渲染。

import { Infer } from "garph";
import { request, gql } from "graphql-request";

import { TodoGQL } from "../../server/gql/schema";
import ListItem from "../ListItem";
import { removeTodo } from "./actions";

const query = gql`
  query getTodos {
    getTodos {
      id
      title
    }
  }
`;

interface QueryData {
  getTodos: Array<Infer<typeof TodoGQL>>;
}

export default async function List() {
  const { getTodos } = await request<QueryData>(
    "http://localhost:3000/api/graphql",
    query
  );

  return (
    <ul className="space-y-4">
      {getTodos?.map((todo) => {
        return (
          <ListItem
            key={todo.id}
            title={todo.title}
            todoId={todo.id}
            removeItem={removeTodo}
          />
        );
      })}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

创建可重用组件后,我们现在可以进入下一步。

路线设置

现在我们已经准备好了所有需要用到的东西,可以开始定义应用程序的路由了。应用程序中将包含的路由如下:

  • app/page.tsx- 应用程序的主路由,我们将在其中列出所有应用的列表,并可以与它们进行交互以删除它们
  • app/new/page.tsx- 表单将出现在哪里以及将验证提交的数据并进行相应修改的操作

现在考虑到这一点,我们可以转到layout.tsx文件并进行以下更改:

import "./globals.css";

export const metadata = {
  title: "Today's tasks",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="h-screen w-screen bg-neutral">
        <section className="container mx-auto p-4">{children}</section>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

接下来,在page.tsx文件中,我们将添加一个Suspense边界,以便我们可以利用 html 流,并且当列表解析异步请求并呈现 html 时,我们将显示一个回退。

import React, { Suspense } from "react";
import Link from "next/link";
import dayjs from "dayjs";

import List from "../components/List";

export default function Page() {
  return (
    <div className="space-y-6">
      <div className="flex flex-row items-start justify-between max-w-xl">
        <span className="space-y-2">
          <h1 className="text-3xl text-primary-content">Today&apos;s tasks</h1>
          <p className="text-lg">{dayjs().format("dddd, D MMM")}</p>
        </span>
        <Link className="btn" href="/new">
          New Task
        </Link>
      </div>

      <Suspense fallback={<span className="loading loading-ring loading-lg" />}>
        <List />
      </Suspense>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

最后,同样重要的是,我们需要创建一个页面,用于在文件夹todo中插入新的表单数据。在这个组件中,我们将创建一个zod模式来验证表单数据,并在此页面中添加一个名为 的函数,该函数应仅在服务器端运行。app/new/page.tsxaddTodo()

import Link from "next/link";
import { redirect } from "next/navigation";
import { GraphQLClient, gql } from "graphql-request";
import { z } from "zod";

const mutation = gql`
  mutation addTodo($title: String!) {
    addTodo(title: $title) {
      id
    }
  }
`;

const formValuesSchema = z.object({
  title: z.string().min(3),
});

async function addTodo(formData: FormData) {
  "use server";

  const formValues = {} as any;
  for (const [key, value] of [...formData.entries()]) {
    if (key.includes("ACTION_ID")) continue;
    formValues[key] = value.valueOf();
  }

  const parsed = await formValuesSchema.parseAsync(formValues);
  const graphQLClient = new GraphQLClient("http://localhost:3000/api/graphql");
  await graphQLClient.request(mutation, parsed);
  redirect("/");
}

export default function Page() {
  return (
    <div className="max-w-xs space-y-6">
      <Link href=".." className="btn btn-ghost">
        Go back
      </Link>

      <form action={addTodo} className="space-y-4">
        <div>
          <label className="label">
            <span className="label-text">Task title</span>
          </label>
          <input
            type="text"
            name="title"
            placeholder="Type here..."
            className="input input-bordered w-full max-w-xs"
            required
            minLength={3}
          />
        </div>

        <button className="btn btn-block" type="submit">
          Submit
        </button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

至此,我结束了本文的最后一步。

结论

我希望您发现这篇文章很有帮助,无论您是在现有项目中使用这些信息还是只是为了好玩而尝试一下。

如果您发现本文有任何错误,请通过评论告诉我。如果您想查看本文的源代码,可以在下方链接的 GitHub 仓库中找到。

Github仓库

鏂囩珷鏉ユ簮锛�https://dev.to/franciscomendes10866/nextjs-and-graphql-the-perfect-combination-for-full-stack-development-18l7
PREV
5 种创新开发工具,助您改善工作流程
NEXT
使用 Next.js、Tanstack Table 和 Typescript 开始使用表格