使用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
|
它仍然会问您几个问题。使用默认设置并完成应用程序的创建。
设置完成后,您可以看到带有应用程序名称的文件夹。让我们进入该文件夹并运行应用程序。
您的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:
您可以转到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上与我联系或访问我的网站。