Next.js部署Vercel后用户注册500错误及数据存储问题求助
Next.js部署Vercel后用户注册API报500错误,无法找到data/users.json且无法持久化用户数据
本地运行时用户注册功能正常,但部署到Vercel后触发注册请求返回500错误,错误日志显示无法找到data/users.json文件,同时需要解决部署后用户信息持久化的问题。
相关代码
components/auths/SignUpModal.tsx
... const onSubmitSignUp = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); try { const signUpBody = { email, lastname, firstname, password, birthday: new Date( `${birthYear}-${birthMonth!.replace('월', '')}-${birthDay}` ).toISOString(), }; signupAPI(signUpBody); } catch (e) { console.log(e); } }; ...
lib/api/index.ts
import Axios from 'axios'; const axios = Axios.create({ baseURL: process.env.NEXT_PUBLIC_URL, }); export default axios;
lib/api/auth.ts
import axios from 'axios'; interface SignUpAPIBody { email: string; firstname: string; lastname: string; password: string; birthday: string; } export const signupAPI = (body: SignUpAPIBody) => { axios.post('api/auth/signup', body); };
pages/api/auth/signup.ts
import { NextApiRequest, NextApiResponse } from 'next'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import Data from '../../../lib/data'; import { StoredUserType } from '../../../types/user'; export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { const { email, firstname, lastname, password, birthday } = req.body; if (!email || !firstname || !lastname || !password || !birthday) { res.statusCode = 400; return res.send('필수 데이터가 없습니다.'); } const userExist = Data.user.exist({ email }); if (userExist) { res.statusCode = 409; return res.send('이미 가입된 이메일입니다.'); } const hashedPassword = bcrypt.hashSync(password, 8); const users = Data.user.getList(); let userId; if (users.length === 0) { userId = 1; } else { userId = users[users.length - 1].id + 1; } const newUser: StoredUserType = { id: userId, email, firstname, lastname, password: hashedPassword, birthday, profileImage: '/static/image/default_user_profile_image.jpg', }; Data.user.write([...users, newUser]); const token = jwt.sign(String(newUser.id), 'my_private_secret'); const expires = new Date(Date.now() + 60 * 60 * 24 * 1000 * 3); res.setHeader( 'Set-Cookie', `access_token=${token};path=/expires=${expires.toUTCString()};httponly` ); res.statusCode = 200; return res.send(newUser); } res.statusCode = 405; return res.end(); };
lib/data/user.ts
import { readFileSync, writeFileSync } from 'fs'; import { StoredUserType } from '../../types/user'; const getList = () => { const usersBuffer = readFileSync('data/users.json'); const usersString = usersBuffer.toString(); if (!usersString) { return []; } const users: StoredUserType[] = JSON.parse(usersString); return users; }; const exist = ({ email }: { email: string }) => { const users = getList(); return users.some((user) => user.email === email); }; const write = async (users: StoredUserType[]) => { writeFileSync('data/users.json', JSON.stringify(users)); }; export default { getList, exist, write };
data/users.json
错误日志
Error: ENOENT: no such file or directory, open 'data/users.json' at Object.openSync (node:fs:600:3) at readFileSync (node:fs:468:35) at getList (/var/task/.next/server/pages/api/auth/signup.js:31:67) at Object.exist (/var/task/.next/server/pages/api/auth/signup.js:45:17) at signup (/var/task/.next/server/pages/api/auth/signup.js:83:33) at Object.apiResolver (/var/task/node_modules/next/dist/server/api-utils/node.js:372:15) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async NextNodeServer.runApi (/var/task/node_modules/next/dist/server/next-server.js:488:9) at async Object.fn (/var/task/node_modules/next/dist/server/next-server.js:751:37) at async Router.execute (/var/task/node_modules/next/dist/server/router.js:253:36) { errno: -2, syscall: 'open', code: 'ENOENT', path: 'data/users.json' } RequestId: 161d1ce4-2d12-4a36-8197-8463b6a18e70 Error: Runtime exited with error: exit status 1 Runtime.ExitError
解决方案
问题根源
Vercel是无服务器(Serverless)部署环境,存在两个核心问题:
- 文件路径问题:部署后的工作目录与本地不同,相对路径
data/users.json无法指向正确位置; - 持久化问题:无服务器函数的执行环境是临时、无状态的,文件系统仅支持临时写入(仅
/tmp目录可写,但重启即丢失),无法持久化存储用户数据。
步骤1:修复文件找不到的临时问题(仅调试用,无法持久化)
修改lib/data/user.ts,使用绝对路径读取文件:
import { readFileSync, writeFileSync } from 'fs'; import path from 'path'; import { StoredUserType } from '../../types/user'; const getList = () => { const filePath = path.join(process.cwd(), 'data/users.json'); try { const usersBuffer = readFileSync(filePath); const usersString = usersBuffer.toString(); return usersString ? JSON.parse(usersString) as StoredUserType[] : []; } catch (err) { return []; } }; const exist = ({ email }: { email: string }) => { const users = getList(); return users.some((user) => user.email === email); }; const write = async (users: StoredUserType[]) => { const filePath = path.join(process.cwd(), 'data/users.json'); writeFileSync(filePath, JSON.stringify(users)); }; export default { getList, exist, write };
步骤2:替换为数据库实现持久化(最终解决方案)
选择Vercel兼容的数据库(如Vercel Postgres、Supabase、PlanetScale),以下以Vercel Postgres为例:
- 安装依赖:
npm install @vercel/postgres
在Vercel控制台创建Postgres数据库,连接参数会自动注入到环境变量。
重写
lib/data/user.ts为数据库操作逻辑:
import { sql } from '@vercel/postgres'; import { StoredUserType } from '../../types/user'; const getList = async (): Promise<StoredUserType[]> => { const { rows } = await sql`SELECT * FROM users`; return rows as StoredUserType[]; }; const exist = async ({ email }: { email: string }): Promise<boolean> => { const { rows } = await sql`SELECT id FROM users WHERE email = ${email}`; return rows.length > 0; }; const write = async (newUser: Omit<StoredUserType, 'id'>): Promise<StoredUserType> => { const { rows } = await sql` INSERT INTO users (email, firstname, lastname, password, birthday, profileImage) VALUES (${newUser.email}, ${newUser.firstname}, ${newUser.lastname}, ${newUser.password}, ${newUser.birthday}, ${newUser.profileImage}) RETURNING * `; return rows[0] as StoredUserType; }; export default { getList, exist, write };
- 修改
pages/api/auth/signup.ts适配异步数据库操作:
import { NextApiRequest, NextApiResponse } from 'next'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import Data from '../../../lib/data'; import { StoredUserType } from '../../../types/user'; export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { const { email, firstname, lastname, password, birthday } = req.body; if (!email || !firstname || !lastname || !password || !birthday) { res.statusCode = 400; return res.send('필수 데이터가 없습니다.'); } const userExist = await Data.user.exist({ email }); if (userExist) { res.statusCode = 409; return res.send('이미 가입된 이메일입니다.'); } const hashedPassword = bcrypt.hashSync(password, 8); const newUser: Omit<StoredUserType, 'id'> = { email, firstname, lastname, password: hashedPassword, birthday, profileImage: '/static/image/default_user_profile_image.jpg', }; const savedUser = await Data.user.write(newUser); const token = jwt.sign(String(savedUser.id), process.env.JWT_SECRET || 'my_private_secret'); const expires = new Date(Date.now() + 60 * 60 * 24 * 1000 * 3); res.setHeader( 'Set-Cookie', `access_token=${token};path=/;expires=${expires.toUTCString()};httponly` ); res.statusCode = 200; return res.send(savedUser); } res.statusCode = 405; return res.end(); };
注意事项
- 无服务器环境下禁止使用本地文件做持久化存储,所有需要持久化的数据必须存入数据库;
- 确保数据库表结构与
StoredUserType类型匹配,比如创建users表时包含自增主键id,以及email、firstname等字段; - JWT密钥建议存入Vercel环境变量,避免硬编码。
内容的提问来源于stack exchange,提问作者seoyoon
相关产品推荐
相关产品推荐

