You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)部署环境,存在两个核心问题:

  1. 文件路径问题:部署后的工作目录与本地不同,相对路径data/users.json无法指向正确位置;
  2. 持久化问题:无服务器函数的执行环境是临时、无状态的,文件系统仅支持临时写入(仅/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为例:

  1. 安装依赖:
npm install @vercel/postgres
  1. 在Vercel控制台创建Postgres数据库,连接参数会自动注入到环境变量。

  2. 重写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 };
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 11:28:10