使用Sevalla部署Next.js API到生产环境的完整指南

本文详细介绍了如何使用Next.js构建RESTful API,并通过Sevalla平台部署到生产环境。包含完整的代码示例、测试方法和部署步骤,适合全栈开发者学习实践。

使用Sevalla部署Next.js API到生产环境

当人们听到Next.js时,通常会想到服务器端渲染、基于React的前端或SEO优化的静态网站。但这个强大框架的功能远不止前端开发。

Next.js还允许开发者在同一代码库中直接构建健壮、可扩展的后端API。这对于中小型应用程序尤其有价值,因为紧密耦合的前端和后端可以加速开发和部署。

在本文中,您将学习如何使用Next.js构建API,并使用Sevalla将其部署到生产环境。通过教程学习构建东西相对容易,但真正的挑战是将其交到用户手中。这样做可以将您的项目从本地原型转变为真实可用的产品。

目录

什么是Next.js?

Next.js是由Vercel构建的开源React框架。它使开发人员能够构建服务器渲染和静态生成的Web应用程序。

它本质上抽象了运行全栈React应用程序所需的配置和样板代码,使开发人员能够更专注于构建功能而不是设置基础设施。

虽然它最初是作为React前端挑战的解决方案,但已发展成为一个全栈框架,让您可以处理后端逻辑、与数据库交互和构建API。这种统一的代码库使得Next.js对现代Web开发特别有吸引力。

安装与设置

让我们安装Next.js。确保您的系统上安装了Node.js和NPM,并且是最新版本。

1
2
3
4
5
$ node --version
v22.16.0

$ npm --version
10.9.2

现在让我们创建一个Next.js项目。命令如下:

1
$ npx create-next-app@latest

上述命令的结果将询问您一系列问题来设置您的应用程序:

1
2
3
4
5
6
7
8
9
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`?  No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*

但对于本教程,我们对全栈应用程序不感兴趣——只对API感兴趣。因此,让我们使用--api标志重新创建应用程序。

1
$ npx create-next-app@latest --api

它仍然会问您几个问题。使用默认设置并完成应用程序的创建。

设置完成后,您可以看到带有应用程序名称的文件夹。让我们进入该文件夹并运行应用程序。

1
$ npm run dev

您的API模板应该在端口3000上运行。转到http://localhost:3000,您应该看到以下消息:

1
2
3
{
  "message": "Hello world!"
}

如何构建REST API

现在我们已经设置了API模板,让我们编写一个基本的REST API。基本的REST API只有四个端点:创建、读取、更新、删除(也称为CRUD)。

通常,我们会使用数据库,但为了简单起见,我们将在API中使用JSON文件。我们的目标是构建一个可以读取和写入此JSON文件的REST API。

API代码将位于项目目录下的/app内。Next.js使用基于文件的路由来构建URL路径。

例如,如果您想要一个URL路径/users,您应该有一个名为"users"的目录,其中包含一个route.ts文件来处理/users的所有CRUD操作。对于/users/:id,您应该在"users"目录下有一个名为[id]的目录,其中包含一个route.ts文件。方括号是告诉Next.js您期望/users/:id路由的动态值。

您还应该在/app/users目录中有users.json,以便您的路由读取和写入数据。

以下是设置的屏幕截图。删除项目中附带的[slug]目录,因为它与我们的内容无关:

![项目结构示意图]

  • 底部的route.ts文件处理/的CRUD操作
  • /users下的route.ts文件处理/users的CRUD操作
  • /users/[id]/下的route.ts文件处理/users/:id的CRUD操作,其中’id’将是动态值
  • /users下的users.json将是我们的数据存储

虽然这种设置对于简单项目来说可能看起来很复杂,但它为大型Web应用程序提供了清晰的结构。如果您想更深入地学习使用Next.js构建复杂API,这里有一个您可以遵循的教程。

/app/route.ts下的代码是我们API的默认文件。您可以看到它处理GET请求并响应"Hello World!":

1
2
3
4
5
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({ message: "Hello world!" });
}

现在我们需要五个路由:

  • GET /users → 列出所有用户
  • GET /users/:id → 列出单个用户
  • POST /users → 创建新用户
  • PUT /users/:id → 更新现有用户
  • DELETE /users/:id → 删除现有用户

以下是/app/users下的route.ts文件的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { promises as fs } from "fs"; // 导入基于promise的文件系统方法
import path from "path"; // 用于处理文件路径

// 定义User对象的结构
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
}

// 定义users.json文件的路径
const usersFile = path.join(process.cwd(), "app/users/users.json");

// 从JSON文件读取用户并将其作为数组返回
async function readUsers(): Promise<User[]> {
  try {
    const data = await fs.readFile(usersFile, "utf-8");
    return JSON.parse(data) as User[];
  } catch {
    // 如果文件不存在或读取失败,返回空数组
    return [];
  }
}

// 将更新的用户数组写入JSON文件
async function writeUsers(users: User[]) {
  await fs.writeFile(usersFile, JSON.stringify(users, null, 2), "utf-8");
}

// 处理GET请求:返回用户列表
export async function GET() {
  const users = await readUsers();
  return NextResponse.json(users);
}

// 处理POST请求:添加新用户
export async function POST(request: NextRequest) {
  const body = await request.json();

  // 解构并验证输入字段
  const { name, email, age } = body as {
    name?: string;
    email?: string;
    age?: number;
  };

  // 如果任何必填字段缺失,返回400
  if (!name || !email || age === undefined) {
    return NextResponse.json(
      { error: "Missing name, email, or age" },
      { status: 400 }
    );
  }

  // 读取现有用户
  const users = await readUsers();

  // 创建基于时间戳的唯一ID的新用户对象
  const newUser: User = {
    id: Date.now().toString(),
    name,
    email,
    age,
  };

  // 将新用户添加到列表并保存到文件
  users.push(newUser);
  await writeUsers(users);

  // 返回新创建的用户,状态为201 Created
  return NextResponse.json(newUser, { status: 201 });
}

现在是/app/users/[id]/route.ts文件的代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { promises as fs } from "fs";
import path from "path";

// 定义User接口
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
}

// users.json文件的路径
const usersFile = path.join(process.cwd(), "app/users/users.json");

// 从JSON文件读取用户的函数
async function readUsers(): Promise<User[]> {
  try {
    const data = await fs.readFile(usersFile, "utf-8");
    return JSON.parse(data) as User[];
  } catch {
    // 如果文件不存在或不可读,返回空数组
    return [];
  }
}

// 将更新的用户写入JSON文件的函数
async function writeUsers(users: User[]) {
  await fs.writeFile(usersFile, JSON.stringify(users, null, 2), "utf-8");
}

// GET /users/:id - 按ID获取用户
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const id = (await params).id;
  const users = await readUsers();

  // 按ID查找用户
  const user = users.find((u) => u.id === id);

  // 如果未找到用户,返回404
  if (!user) {
    return NextResponse.json({ error: "User not found" }, { status: 404 });
  }

  // 返回找到的用户
  return NextResponse.json(user);
}

// PUT /users/:id - 按ID更新用户
export async function PUT(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const id = (await params).id;
  const body = await request.json();

  // 从请求体中提取可选字段
  const { name, email, age } = body as {
    name?: string;
    email?: string;
    age?: number;
  };

  const users = await readUsers();

  // 查找要更新的用户的索引
  const index = users.findIndex((u) => u.id === id);

  // 如果未找到用户,返回404
  if (index === -1) {
    return NextResponse.json({ error: "User not found" }, { status: 404 });
  }

  // 仅使用提供的字段更新用户
  users[index] = {
    ...users[index],
    ...(name !== undefined ? { name } : {}),
    ...(email !== undefined ? { email } : {}),
    ...(age !== undefined ? { age } : {}),
  };

  await writeUsers(users);

  // 返回更新的用户
  return NextResponse.json(users[index]);
}

// DELETE /users/:id - 按ID删除用户
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const id = (await params).id;
  const users = await readUsers();

  // 查找要删除的用户的索引
  const index = users.findIndex((u) => u.id === id);

  // 如果未找到用户,返回404
  if (index === -1) {
    return NextResponse.json({ error: "User not found" }, { status: 404 });
  }

  // 从数组中删除用户并保存更新后的列表
  const [deleted] = users.splice(index, 1);
  await writeUsers(users);

  // 返回删除的用户
  return NextResponse.json(deleted);
}

我们将在/app/users.json中有一个空数组。您可以在此存储库中找到所有代码。

如何测试API

现在让我们测试API端点。

首先,运行API:

1
$ npm run dev

您可以转到http://localhost:3000/users,可以看到一个空数组,因为我们还没有推送任何用户信息。

从代码中,我们可以看到用户对象需要name、email和age,因为id在POST端点中自动生成。

我们将使用Postman模拟对API的请求,并确保API按预期运行。

GET /users:第一次尝试时将是空的,因为我们还没有推送任何数据。

![GET /users空响应截图]

POST /users:创建新用户。在"body"下,选择"raw"并选择"JSON"。这是我们将发送给api的数据。JSON正文将是:

1
{"name":"Manish","age":30, "email":"manish@example.com"}

我将再创建一个名为"Larry"的记录。以下是JSON:

1
{"name":"Larry","age":25, "email":"larrry@example.com"}

现在让我们看看用户。您应该看到对/users的GET请求有两个条目:

![GET /users有两个用户的截图]

现在让我们使用/users/:id查看单个用户。

![GET /users/:id截图]

现在让我们将Larry的年龄更新为35。我们将仅使用PUT请求到/users/:id在请求正文中传递年龄。

![PUT /users/:id截图]

现在让我们删除Larry的记录。

![DELETE /users/:id截图]

如果您检查/users,您应该只看到一个记录:

![GET /users只有一个用户的截图]

所以我们已经构建并测试了我们的api。现在让我们将其上线。

如何部署到Sevalla

Sevalla是一个现代的、基于使用的平台即服务提供商,是Heroku等网站或您在AWS上自管理设置的替代方案。它结合了强大的功能和平滑的开发人员体验。

Sevalla为您的项目提供应用程序托管、数据库、对象存储和静态站点托管。它附带了一个慷慨的免费层,所以让我们看看如何使用Sevalla将我们的API部署到云端。

确保您的代码已提交到GitHub,或为此项目fork我的存储库。如果您是Sevalla的新用户,可以使用您的GitHub帐户注册以启用从GitHub帐户的直接部署。每次您将代码推送到项目时,Sevalla都会自动拉取并将您的应用程序部署到云端。

登录Sevalla后,单击"Applications"。现在让我们创建一个应用程序。

![Sevalla应用创建界面]

如果您已通过GitHub进行身份验证,应用程序创建界面将显示存储库列表。选择您推送代码的存储库,或者如果您从我的存储库fork了它,则选择nextjs-api项目。

选中"auto deploy on commit"框。这将确保您的最新代码自动部署到Sevalla。现在,让我们选择可以部署应用程序的实例。每个实例都有自己的定价,基于服务器的容量。

让我们选择每月5美元的hobby服务器。Sevalla为我们提供50美元的免费层,因此我们不需要支付任何费用,除非我们超过此使用层。

![实例选择界面]

现在,单击"Create and Deploy"。这应该从我们的存储库中拉取代码,运行构建过程,设置Docker容器,然后部署应用程序。通常是系统管理员的工作,由Sevalla完全自动化。

等待几分钟完成以上所有操作。您可以在"Deployments"界面中查看日志。

![部署日志界面]

现在,单击"Visit App",您将获得API的实时URL(以sevalla.app结尾)。您可以将"http://localhost:3000"替换为新URL,并使用Postman运行相同的测试。

恭喜——您的应用程序现已上线。您可以使用管理界面为应用程序做更多事情,例如:

  • 监控应用程序的性能
  • 查看实时日志
  • 添加自定义域
  • 更新网络设置(打开/关闭端口以增强安全性等)
  • 添加更多存储

Sevalla还提供对象存储、数据库、缓存等资源,这些超出了本教程的范围。但它让您可以监控、管理和扩展应用程序,而无需系统管理员。这就是PaaS系统的美妙之处。这里是VPS与PaaS系统用于应用程序托管的详细比较。

结论

在本文中,我们超越了Next.js的典型前端用例,并探索了其作为全栈框架的功能。我们使用App Router和基于文件的路由构建了一个完整的REST API,数据存储在JSON文件中。然后,我们更进一步,使用Sevalla(一个现代化的PaaS,自动化部署、扩展和监控)将API部署到生产环境。

这种设置展示了开发人员如何在单个Next.js项目中构建和交付全栈应用程序,如前端、后端和部署。无论您是进行原型设计还是构建规模,此工作流程都为您提供了所需的一切,以快速高效地将应用程序交到用户手中。

希望您喜欢这篇文章。我很快就会带来另一篇文章。在LinkedIn上与我联系或访问我的网站。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计