如何使用 Fly.io 和 Tigris 部署 Next.js 应用

在本教程中,您将学习到应用部署平台 Fly.io 和全球分布式的 S3 兼容对象存储服务 Tigris。

这两个平台密切相关,使它们成为您项目的绝佳选择。您可以从 Fly.io 获得应用部署体验,并从 Tigris 获得对象存储功能。

应用部署相当简单易懂,因此让我们首先快速介绍一下 Tigris 使用的存储桶存储。

(本文视频讲解:java567.com)

先决条件

  • 安装了 IDE/代码编辑器,如 Visual Studio Code
  • 安装了 Node.js 和 npm
  • 安装或设置了 Next.js
  • 在 Fly.io 上有一个免费账户
  • 在 Tigris 上有一个免费账户

目录

  1. 什么是存储桶?
  2. 我们将要构建什么
  3. 如何在 Fly.io 和 Tigris 上创建账户
  4. 如何设置用户数据库项目
  5. 如何构建用户数据库应用程序
  6. 如何创建用户数据库界面
  7. 如何将您的应用程序部署到 Fly.io
  8. 结论

什么是存储桶?

Amazon S3 存储桶是通过亚马逊网络服务(AWS)的简单存储服务(S3)平台访问的公共云存储资源。

全球分布的、与 S3 兼容的对象存储服务 Tigris 使用低延迟存储功能。这意味着您可以在 Tigris 上访问亚马逊的 S3 存储桶来满足您的存储需求。

Tigris 还已经与 Fly.io 完全集成,并且也与运行在 Fly.io 硬件上的 flyctl 完全集成。Fly.io 的命令行界面 flyctl 允许您从账户创建到应用程序部署的所有操作都可以处理。

我们将构建什么

为了学习这些平台的基础知识,我们将构建一个用户数据库应用程序。这相当直观:基本上我们可以执行完整的 CRUD 请求,这意味着可以读取、添加、更新和删除用户数据。

Next.js 将是我们的主要框架,因为它允许我们构建全栈应用,而无需创建单独的服务器。

用户数据库应用用户数据库应用首页

您可以从他们的文档了解更多关于 Fly.io 和 Tigris 的信息。为了这个项目,我们需要在两个平台上创建一个账户,我会在一分钟内为您介绍。

所以现在,理论部分已经讲解完毕,让我们开始吧。

您可以在我的 GitHub 上找到这个项目的代码库。

如何在 Fly.io 和 Tigris 上创建账户

只需按照以下步骤在两个平台上开始运行:

  1. 首先您需要在 Fly.io 上创建一个账户,因为要使用 Tigris 您需要一个 Fly.io 的账户。
  2. 接下来,在您的计算机上安装 flyctl 命令行工具,这对于设置您的账户以部署应用程序至关重要。

好的,让我们继续下一个阶段,我们将在那里设置我们的项目以及我们的 Tigris 存储桶。

如何设置用户数据库项目

首先,导航到您计划创建项目的计算机上的目录。然后创建一个名为 fly-tigris-user-database 的文件夹并进入它。现在在该文件夹内运行以下命令来设置一个 Next.js 项目:

npx create-next-app .

我们正在做的就是设置我们的 Next.js 项目,重要的是对配置选择是为 Tailwind CSS 和 App router 选择是,因为我们在这个项目中需要它们。

现在运行以下命令来安装 AWS SDK:

npm install @aws-sdk/client-s3

我们只需要安装一个包(@aws-sdk/client-s3),这是连接到我们的存储桶所需的。

好的,很好 - 现在是时候为我们刚刚创建的项目创建一个存储桶了。您可以在它们的官方文档中找到此过程的指导。

只需运行以下命令来创建一个存储桶:

fly storage create

现在在设置屏幕上,选择一个名称给您的存储桶。名称应该是唯一的,所以您不能使用其他人选择的名称。

好的,现在是最重要的阶段:您应该有您的 AWS 和存储桶的密钥,就像这个例子中所示:

AWS_ACCESS_KEY_ID: 您的密钥
AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
AWS_REGION: auto
AWS_SECRET_ACCESS_KEY: 您的秘密访问密钥
BUCKET_NAME: 您的存储桶名称

在您的 Next.js 项目根目录下创建一个 .env.local 文件,然后将所有这些秘密环境变量复制粘贴进去。

为了使这些环境变量在我们的 Next.js 应用程序中正常工作,我们需要调整它们的名称使其公开。查看下面的示例,并对您的 .env.local 文件进行更改。

NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID: 您的密钥
NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
NEXT_PUBLIC_SECRET_AWS_REGION: auto
NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY: 您的秘密访问密钥
NEXT_PUBLIC_SECRET_BUCKET_NAME: 您的存储桶名称

现在在 Tigris 文档页面上,如果您点击仪表板按钮并登录您的账户,您应该会看到您新创建的存储桶,就像我的示例中所示:

Tigris 存储桶Tigris 网站存储桶管理页面

太棒了!第一阶段完成了。我们现在有一个存储桶来在线存储我们的应用数据,所以我们可以开始在下一节中创建我们的应用程序。

如何构建用户数据库应用程序

我将把这一部分分成两个部分。首先,我们将建立并运行服务器,以便测试 CRUD 端点。然后我们将完成前端部分。

如何创建用户数据库服务器

首先,让我们创建我们的后端架构。我们将创建四个端点,一个用于每个 CRUD 请求。我们还需要一个辅助文件,其中包含一些用于从我们的对象存储中获取用户的函数。

如果您尚未这样做,请cd到项目的根目录,并运行下面的命令。它们将快速设置我们的所有文件和文件夹:

cd src/app
mkdir api
mkdir api/deleteuser api/getusers api/postuser api/updateuser
touch api/deleteuser/route.js
touch api/getusers/route.js
touch api/postuser/route.js
touch api/updateuser/route.js
mkdir helpers
touch helpers/getUsers.js

好的,很快就完成了。现在我们只需要将代码添加到我们的五个文件中,我们的后端 API 就准备好测试了。

首先让我们处理辅助文件。这个文件中的代码允许我们收集并访问存储在 Tigris 上我们 S3 存储桶中的用户数据。

把下面的代码放到 helpers/getUsers.js 中:

import {S3Client,ListObjectsV2Command,GetObjectCommand,
} from '@aws-sdk/client-s3';const streamToString = (stream) =>new Promise((resolve, reject) => {const chunks = [];stream.on('data', (chunk) => chunks.push(chunk));stream.on('error', reject);stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));});export async function fetchAllUsersFromS3() {try {const s3 = new S3Client({region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,credentials: {accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,},});const commandDetails = new ListObjectsV2Command({Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,MaxKeys: 10,});const { Contents } = await s3.send(commandDetails);console.log('List Result', Contents);if (!Contents) {console.log('no users');} else {const users = await Promise.all(Contents.map(async (item) => {const getObject = new GetObjectCommand({Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,Key: item.Key,});const { Body } = await s3.send(getObject);const data = await streamToString(Body);const userObject = JSON.parse(data);console.log('Data', data);return userObject;}));return users;}} catch (e) {console.error(e);throw e;}
}export async function getUserById(users, userId) {if (!users) {console.log('no users');} else {return users.find((user) => user.id === userId);}
}export async function getUserByIdEmail(users, email) {if (!users) {console.log('no users');} else {return users.find((user) => user.email.toLowerCase() === email.toLowerCase());}
}

主要的函数是 fetchAllUsersFromS3(),它基本上使用我们需要的凭证和配置创建 S3 客户端。它使用 GetObjectCommand 获取对象的内容,然后使用 streamToString 函数将其从流转换为字符串。然后解析 JSON 数据返回用户对象。

另外两个函数调用,getUserById(users, userId)getUserByIdEmail(users, email),是允许我们根据 ID 或电子邮件地址在我们的 S3 存储桶中搜索用户的辅助函数。代码将基本的 AWS 配置参数存储在环境变量中,包括区域、端点 URL、访问密钥和秘密访问密钥,以及 S3 存储桶名称。

好了,现在只剩下路由了。

首先把这段代码放到 getusers/route.js 中:

import {S3Client,ListObjectsV2Command,GetObjectCommand,
} from '@aws-sdk/client-s3';export async function GET() {const streamToString = (stream) =>new Promise((resolve, reject) => {const chunks = [];stream.on('data', (chunk) => chunks.push(chunk));stream.on('error', reject);stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));});try {const s3 = new S3Client({region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,credentials: {accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,},});const listParams = {Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,MaxKeys: 10,};const list = new ListObjectsV2Command(listParams);const { Contents } = await s3.send(list);console.log('List Result', Contents);if (!Contents || Contents.length === 0) {console.log('No users found');return new Response(JSON.stringify({ error: 'No users found' }), {status: 404,});}const users = await Promise.all(Contents.map(async (item) => {const getObjectParams = {Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,Key: item.Key,};const getObject = new GetObjectCommand(getObjectParams);const { Body } = await s3.send(getObject);const data = await streamToString(Body);console.log('Backend API GET Data:', data);return JSON.parse(data);}));return new Response(JSON.stringify(users), { status: 200 });} catch (e) {console.error('Error:', e);return new Response(JSON.stringify({ error: e.message || 'Unknown error' }),{ status: 500 });}
}

在这个文件的代码中,我们从 Tigris S3 存储桶中检索用户数据,并通过 Next.js API 路由处理程序函数将其作为 JSON 响应返回。

该函数还导入了与我们的 S3 存储桶通信所需的 AWS SDK 客户端。当请求 API 路由时,将调用主入口点,即 GET 函数。使用环境变量,GET 方法首先建立了一个 S3 客户端,其中包含所需的区域、端点和凭证设置。

之后,创建了一个 ListObjectsV2Command 来从指定的 S3 存储桶中检索用户数据列表的项目。然后方法通过对象列表,使用 GetObjectCommand 获取每个对象的数据。

streamToString 方法用于将每个对象的内容从流转换为字符串。

在解析 JSON 输入后,用户对象作为 JSON 响应发送出去。如果未检测到用户,则返回一个 404 错误响应,并提供一个错误消息。如果在过程中出现任何问题,则提供一个 500 错误响应,其中包含错误消息。

接下来是 POST 路由,将以下代码放入 postuser/route.js 中:

import { fetchAllUsersFromS3, getUserByIdEmail } from '../../helpers/getUsers';import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';export async function POST(req) {try {const { firstname, lastname, email, password } = await req.json();const id = crypto.randomUUID();const data = { firstname, lastname, email, password, id };console.log('Request body data', data);const allUsers = await fetchAllUsersFromS3();console.log('all users', allUsers);const existingUser = await getUserByIdEmail(allUsers, email);console.log(existingUser, email);if (existingUser) {return Response.json({error: 'Email address already in use',});}const s3 = new S3Client({region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,credentials: {accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,},});const commandDetails = new PutObjectCommand({Body: JSON.stringify(data),Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,Key: email,});await s3.send(commandDetails);return Response.json({ message: 'User added' });} catch (e) {console.error(e);return Response.json({ error: 'Failed to create user' });}
}

此代码用于管理用户注册,并将用户信息保存在我们的 Tigris S3 存储桶中。fetchAllUsersFromS3getUserByIdEmail 这两个辅助函数被导入,用于与 S3 存储桶通信并收集用户数据。初始入口点,即当通过 API 路由发出 POST 请求时调用的是 POST 函数。

首先,POST 函数从请求体中提取用户信息(名、姓、电子邮件和密码)。然后使用 crypto.randomUUID() 创建一个唯一的用户 ID。为了获取 S3 存储桶中的所有当前用户数据,该方法调用 fetchAllUsersFromS3 辅助函数。

代码使用 getUserByIdEmail 辅助函数来确定请求中提供的电子邮件地址是否已经存在于用户数据中。如果电子邮件地址已经存在,则该方法提供一个包含错误消息的 JSON 响应。如果电子邮件地址是唯一的,则使用环境变量创建具有所需设置的 S3 客户端(区域、端点和凭证)。

然后生成一个 PutObjectCommand,将新的用户数据(作为 JSON 字符串)上传到 S3 存储桶,其中电子邮件地址是键。最后,该方法生成一个 JSON 响应,确认用户已成功添加。如果在过程中发生错误,则该函数提供一个包含错误消息的 JSON 响应。

紧接着是我们的 UPDATE 路由,将以下代码放入 updateuser/route.js 中:

import { getUserById, fetchAllUsersFromS3 } from '../../helpers/getUsers';import {S3Client,DeleteObjectCommand,PutObjectCommand,
} from '@aws-sdk/client-s3';export async function PUT(req) {try {const { firstname, lastname, email, originalEmail, id } = await req.json();console.log('request data', firstname, lastname, email, originalEmail, id);const allUsers = await fetchAllUsersFromS3();console.log('all users', allUsers);const userToUpdate = await getUserById(allUsers, id);console.log('user to update', userToUpdate);const user = allUsers.find((user) => user.id === id);const userEmail = user ? user.email : null;console.log('User Email', userEmail);if (!userToUpdate) {return Response.json({ error: 'User not found' });}if (!originalEmail || !email) {return Response.json({error: 'Both originalEmail and email are required for update',});}const data = { firstname, lastname, email, id };console.log('Updated data', data);const s3 = new S3Client({region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,credentials: {accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,},});console.log('Original email', originalEmail);console.log('New email', email);if (userEmail === originalEmail) {console.log('The emails are the same so its a match');const deleteCommand = new DeleteObjectCommand({Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,Key: originalEmail,});await s3.send(deleteCommand);const putCommand = new PutObjectCommand({Body: JSON.stringify(data),Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,Key: email,});await s3.send(putCommand);return Response.json({ message: 'User updated successfully' });} else {console.log('Error: The emails do not match');return Response.json({ error: 'Failed to update user' });}} catch (e) {console.error(e);}
}

该代码包括之前的相同辅助函数 getUserByIdfetchAllUsersFromS3,用于连接 S3 存储桶并获取用户数据。主要入口点是 PUT 函数,每当向 API 路由发出 PUT 请求时就会调用它。PUT 函数首先从请求体中提取用户数据(名、姓、电子邮件、原始电子邮件和 ID)。

然后它调用 fetchAllUsersFromS3 辅助函数,从 S3 存储桶中检索所有现有的用户数据。然后代码通过使用指定的用户 ID 调用 getUserById 辅助函数来找到要更新的用户。如果找不到用户,则该方法提供一个包含错误消息的 JSON 响应。

如果原始电子邮件或电子邮件不存在,则该函数提供一个包含错误消息的 JSON 响应。然后方法根据提供的信息构建一个更新后的用户数据对象。此外,代码可以使用环境变量创建具有所需设置(区域、端点和凭证)的 S3 客户端。

假设初始电子邮件地址与当前用户的电子邮件地址匹配,该函数将更新用户信息;但是如果原始电子邮件与当前用户的电子邮件地址不匹配,则方法提供一个包含错误消息的 JSON 响应。像之前一样,如果在过程中检测到错误,则该函数将创建一个日志而不发送响应。

最后是我们的 DELETE 路由。将以下代码添加到 deleteuser/route.js 中:

import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';import { fetchAllUsersFromS3, getUserById } from '../../helpers/getUsers';export async function DELETE(req) {try {const id = await req.json();console.log('Id', id.id);const allUsers = await fetchAllUsersFromS3();console.log('all users', allUsers);const userToDelete = await getUserById(allUsers, id.id);console.log('user to delete', userToDelete);if (!userToDelete) {return Response.json({ error: 'User not found' });}const userEmail = userToDelete.email;const s3 = new S3Client({region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,credentials: {accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,},});const deleteCommand = new DeleteObjectCommand({Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,Key: userEmail,});await s3.send(deleteCommand);return Response.json({ message: 'User deleted successfully' });} catch (e) {console.error(e);return Response.json({ error: 'Failed to delete user' });}
}

这用于从我们的存储桶中删除数据。代码包括与 S3 通信所需的相同的 AWS SDK 客户端,以及用于从 S3 存储桶中获取用户数据的两个辅助函数 fetchAllUsersFromS3getUserById。关键入口点是 DELETE 函数,当向 API 路由发出 DELETE 请求时调用它。

DELETE 函数中,首先从请求体中提取用户 ID。然后它调用 fetchAllUsersFromS3 辅助方法,从 S3 存储桶中检索所有现有的用户数据。然后代码通过使用指定的用户 ID 调用 getUserById 辅助函数来找到要删除的用户。如果客户端找不到,则该方法提供一个包含错误消息的 JSON 响应。

然后生成一个 DeleteObjectCommand,从 S3 存储桶中删除该项,其中用户的电子邮件地址是键。最后,该方法生成一个 JSON 响应,确认用户已成功删除。如果在过程中检测到错误,则该函数将创建一个日志而不发送响应。

前端部分

现在我们已经创建了我们的 API,我们将构建一个简单的前端来测试我们的功能。我们将使用 React 和 Tailwind CSS 来实现。

首先,确保您已经安装了 create-next-app。如果没有,请运行以下命令:

npm install -g create-next-app

接下来,在终端中运行以下命令创建一个新的 Next.js 项目:

npx create-next-app@latest my-next-app

现在,我们将进入项目文件夹并安装一些依赖项:

cd my-next-app
npm install aws-sdk tailwindcss

接下来,我们将配置 Tailwind CSS。在项目根目录下创建一个 tailwind.config.js 文件,内容如下:

module.exports = {purge: [],darkMode: false, // or 'media' or 'class'theme: {extend: {},},variants: {extend: {},},plugins: [],
}

然后,在 styles/globals.css 中,添加以下内容:

@tailwind base;
@tailwind components;
@tailwind utilities;

接下来,让我们在项目中创建一个简单的注册表单。在 pages 目录下创建一个名为 index.js 的文件,并将以下代码粘贴到该文件中:

import { useState } from 'react';export default function Home() {const [form, setForm] = useState({firstname: '',lastname: '',email: '',password: '',});const handleChange = (e) => {const { name, value } = e.target;setForm((prevState) => ({...prevState,[name]: value,}));};const handleSubmit = async (e) => {e.preventDefault();try {const response = await fetch('/api/postuser', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify(form),});if (response.ok) {const data = await response.json();console.log(data);}} catch (error) {console.error('Error:', error);}};return (<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"><div className="max-w-md w-full space-y-8"><div><h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Register</h2></div><form className="mt-8 space-y-6" onSubmit={handleSubmit}><input type="hidden" name="remember" defaultValue="true" /><div className="rounded-md shadow-sm -space-y-px"><div><label htmlFor="firstname" className="sr-only">First name</label><inputid="firstname"name="firstname"type="text"autoComplete="given-name"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"placeholder="First name"value={form.firstname}onChange={handleChange}/></div><div><label htmlFor="lastname" className="sr-only">Last name</label><inputid="lastname"name="lastname"type="text"autoComplete="family-name"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"placeholder="Last name"value={form.lastname}onChange={handleChange}/></div><div><label htmlFor="email" className="sr-only">Email address</label><inputid="email"name="email"type="email"autoComplete="email"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"placeholder="Email address"value={form.email}onChange={handleChange}/></div><div><label htmlFor="password" className="sr-only">Password</label><inputid="password"name="password"type="password"autoComplete="current-password"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"placeholder="Password"value={form.password}onChange={handleChange}/></div></div><div><buttontype="submit"className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"><span className="absolute left-0 inset-y-0 flex items-center pl-3">{/* Heroicon name: solid/lock-closed */}<svgclassName="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"xmlns="http://www.w3.org/2000/svg"viewBox="0 0 20 20"fill="currentColor"aria-hidden="true"><pathfillRule="evenodd"d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><pathfillRule="evenodd"d="M4 8a6 6 0 1112 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2V8zm2-2v5a2 2 0 002 2h6a2 2 0 002-2V6a2 2 0 00-2-2H8a2 2 0 00-2 2z"clipRule="evenodd"/></svg></span>Register</button></div></form></div></div>);
}

这将创建一个简单的注册表单,允许用户输入名、姓、电子邮件和密码。提交表单将通过 API 路由将用户数据发送到我们的 Next.js 应用程序的服务器端。

现在,您可以运行您的 Next.js 应用程序并测试 API 功能。要运行应用程序,请在终端中执行以下命令:

npm run dev

这将启动本地开发服务器。现在,您可以打开浏览器并访问 http://localhost:3000,然后尝试在注册表单中输入数据并提交以查看 API 是否正常工作。

完成这个项目后,您可以进一步扩展它,例如,创建登录表单、更多 API 路由以支持其他操作,或是添加用户数据的验证。

(本文视频讲解:java567.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/2417.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ECC(椭圆曲线密码学)和DH(迪菲-赫尔曼密钥交换)

目录 ECC(椭圆曲线密码学)和DH(迪菲-赫尔曼密钥交换) ECDHE和ECC在密码学领域

前端的一些3D旋转效果和实例——table切换

首先&#xff0c;讲一下有关3D旋转的属性&#xff1a; transform用于设置变形,有以下参数 1、translate(x,y) 2D的设置移动距离&#xff0c;x表示沿x轴的距离&#xff0c;y表示沿y轴的距离&#xff08;y轴是向下为正&#xff09;不写代表0 translateX(x) translateY(y) tr…

题解:力扣704/35/34

1.力扣704 : 二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。示例 1:输入: nums [-1,0,3,5,9,12], target 9 …

公司项目协作Git的使用

声明:因本人技术有限,这篇文章里面可能会有一些错误,希望发现的同仁能够指出,还望大家海涵。如果你觉得这个对你来说完全小儿科,你也可以无视,这篇教程只是给对git还不是很熟悉,不懂如何使用版本工具的同仁予以参考。 1、目的 由于我们的项目都是多人合作,在编写代码的…

2001-2021年上市公司制造业智能制造词频统计数据

2001-2021年上市公司制造业智能制造词频统计数据 1、时间&#xff1a;2001-2021年 2、来源&#xff1a;上市公司年报 3、指标&#xff1a;年份、股票代码、行业名称、行业代码、所属省份、所属城市、智能制造词频、智能制造占比(%) 4、范围&#xff1a;上市公司 5、样本量…

高精度算法讲解以及相关练习题

概念 在我们进行计算的过程中&#xff0c;经常会遇到几十位&#xff0c;甚至几百位的数字的计算问题&#xff0c;也有可能会遇到小数点后几十位&#xff0c;几百位的情况&#xff0c;而我们面对这样的情况下&#xff0c;long long和double的数据范围显然是不够使用的了。因此这…

MATLAB初学者入门(4)—— 高级特性和实用技巧

继续深入MATLAB优化模型求解&#xff0c;我们可以探索更多的高级特性和实用技巧&#xff0c;这有助于提高求解效率和结果的精确度。以下内容将涉及到一些高级配置选项、多目标优化和稳健性优化。 高级配置选项 在使用fmincon或其他MATLAB优化求解器时&#xff0c;你可以配置多…

playwright-断言

前言 playwright 提供了一个 expect方法 用于断言 断言 断言描述expect(locator).to_be_checked()Checkbox已勾选expect(locator).to_be_disabled()元素被禁用expect(locator).to_be_editable()元素可编辑expect(locator).to_be_empty()内容为空expect(locator).to_be_enable…

探索嵌入式系统:快速入门指南概览

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

OpenHarmony实战开发-性能测试工具SmartPerf Editor使用指导

概述 SmartPerf Editor是一款PC端桌面应用&#xff0c;通过监测、采集应用运行时FPS、CPU、GPU、Memory、Battery、Network等性能数据&#xff0c;帮助开发者了解应用的性能状况。SmartPerf Editor还集成了DrawingDoc功能&#xff0c;可录制Render Service绘制指令&#xff0c…

Android Perfetto 监控应用启动耗时

Perfetto 是一个 Google 开发的用于安卓系统性能监控和调试的工具&#xff0c;它旨在提供实时数据收集和可视化功能&#xff0c;帮助我们分析和优化应用程序的性能表现。Perfetto 可以捕获系统事件、CPU、内存、网络、GPU 等性能指标数据&#xff0c;并将其记录为轻量级的 Trac…

开发环境中的调试视图(IDEA)

当程序员写完一个代码时必然要运行这个代码&#xff0c;但是一个没有异常的代码却未必满足我们的要求&#xff0c;因此就要求程序员对已经写好的代码进行调试操作。在之前&#xff0c;如果我们要看某一个程序是否满足我们的需求&#xff0c;一般情况下会对程序运行的结果进行打…

茶饮门店本地生活抖音团购运营方案计划书

【干货资料持续更新&#xff0c;以防走丢】 茶饮门店本地生活抖音团购运营方案计划书 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 PPT可编辑80页&#xff08;完整资料包含以下内容&#xff09; 目录 抖音本地生活运营方案 1. 账号基础搭建与优化 - 门店账号…

练习题(2024/4/23)

1分发糖果 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求&#xff0c;给这些孩子分发糖果&#xff1a; 每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获得更多的糖果。 请你给每个孩子分发糖果&#xff0c;计算并返回…

Php-WebView 现代跨平台 GUI分享

GitHub :php-webview 一个用于 C/C 的小型跨平台 Web 视图库&#xff0c;用于构建现代跨平台 GUI。 该项目的目标是为最广泛使用的平台创建一个通用的 HTML5 UI 抽象层。 它支持双向 JavaScript 绑定&#xff08;从 C/C 调用 JavaScript 和从 JavaScript 调用 C/C&#xff09;。…

ClickHouse 高可用之副本

文章目录 ClickHouse 副本支持副本的引擎配置高可用副本副本应用1.副本表概述2.创建副本表3.写入模拟数据4.副本验证 扩展 —— 在 Zookeeper 中查看副本表信息 ClickHouse 副本 ClickHouse 通过副本机制&#xff0c;可以将数据拷贝存储在不同的节点上。这样&#xff0c;如果一…

Linux登录后的提示信息在哪里配置及配置方法

一、在哪配置 首先&#xff0c;用户的个性化提示信息可以通过编辑用户的.bashrc或.bash_profile文件来实现。这些文件通常位于用户的主目录下。你可以使用文本编辑器&#xff08;如vi、nano或gedit&#xff09;打开这些文件&#xff0c;并在文件的末尾添加你想要的个性化提示信…

我的世界服务器设计思路应该是什么样?

我的世界服务器设计思路可以从这4个方面展开&#xff1a;1.选择你喜欢的东西&#xff1b;2.认识你的极限&#xff1b;3.注入新鲜元素&#xff1b;4.让服务器变得享受且有回报。 1.选择你喜欢的东西 设计服务器的首要规则是创造一些你自己会积极享受玩的东西。没有人愿意花费宝…

在Spring boot中指定随机可用的端口

​ 正常情况下每个spring boot启动都有固定的端口&#xff0c;也就是8080&#xff0c;如果启动多个项目&#xff0c;很容易出现端口冲突&#xff0c;那么怎么解决这个问题呢&#xff1f; 解决方案1&#xff1a; random 随机端口 ​ 在spring boot中&#xff0c;可以通过${ran…

linux的一些实用操作

快捷键 强制停止 ctrlc强制停止或退出命令的输入 退出登出 ctrld强制退出用户登录或退出某些程序的专属页面&#xff08;如py&#xff09; ps&#xff1a;不能退出vi/vim 历史命令搜索 history可以查看历史命令&#xff0c;用来复制粘贴 在使用history之后&#xff0c;…