nodejs 项目模板

初始化项目

cd nodejs-vercel-template
npm init -y

创建目录结构

touch vercel.json
touch .env
touch .env.example
touch .gitignore
touch README.md
mkdir api
mkdir frontend
mkdir public
touch api/index.js

编辑package.json文件

{
  "name": "vercel-node-vue-bizhi", // 项目名称
  "version": "1.0.0", // 项目版本
  "description": "A node-vue-bizhi project", // 项目描述
  "scripts": {
    "dev": "npm run dev:backend & npm run dev:frontend", // 开发模式
    "dev:backend": "nodemon api/index.js", // 后端开发模式
    "dev:frontend": "cd frontend && npm run dev",  // 前端开发模式
    "build": "cd frontend && npm install && npm run build", // 构建前端
    "start": "node api/index.js" // 启动后端
  },
  "keywords": [
    "nodejs",
    "vue",
    "vercel",
    "supabase"
  ],
  "author": "WenYan",
  "license": "MIT",
  "dependencies": {  // 项目依赖
    "@supabase/supabase-js": "^2.39.3",
    "cors": "^2.8.5",
    "dotenv": "^16.4.1",
    "express": "^4.18.2"
  },
  "devDependencies": { // 开发依赖
    "nodemon": "^3.0.3"
  }
}

安装依赖

npm install

配置api/index.js

修改index.js,引入express,并设置api和路由

require('dotenv').config();  // 加载环境变量
const express = require('express'); // 引入express
const cors = require('cors'); // 引入cors
const testRouter = require('./routers/test');

const app = express();
const PORT = process.env.PORT || 3000;

// 设置中间件 middleware
// - cors
const corsOptions = {
    origin: function (origin, callback) {
        // 允许列表
        const allowedOrigins = [
            /\.vercel\.app$/, // vercel域名
            process.env.FRONTEND_URL, //自定义url
        ].filter(Boolean); // 过滤掉undefined

        // 开发环境允许所有
        if (process.env.NODE_ENV === 'development') {
            callback(null, true);
            return;
        }

        // 生产环境检查
        if (!origin) {
            callback(null, true);
            return;
        }

        // 检查是否在允许列表中
        const isAllowed = allowedOrigins.some(allowedOrigin => {
            if (allowedOrigin instanceof RegExp) {
                return allowedOrigin.test(origin);
            }
            return allowedOrigin === origin;
        });

        if (isAllowed) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true // 允许携带凭证(cookies)
};
app.use(cors(corsOptions)); // 跨域检查

// - bodyParser
app.use(express.json()); // 解析json 请求
app.use(express.urlencoded({ extended: true })); // 解析url 编码

// - 指定静态文件目录 frontend/dist, frontend使用vue生成静态文件,单页面应用
// app.use(express.static(path.join(__dirname, '../frontend/dist')));

// 拆分路由组件
app.use('/api/v1', testRouter);

// 404处理
app.use('/api/*', (req, res) => {
    res.status(404).json({
        code: 404,
        message: 'API endpoint not found',
        data: null
    });
});

// Start server (only in development)
if (process.env.NODE_ENV !== 'production') {
    app.listen(PORT, () => {
        console.log('\n' + '='.repeat(60));
        console.log(` Backend server running on http://localhost:${PORT}`);
        console.log(` API available at http://localhost:${PORT}/api/v1`);
        console.log(' Hot reload enabled with nodemon');
        console.log('='.repeat(60) + '\n');
    });
}

// Export for Vercel
module.exports = app;

创建路由

mkdir api/routers
touch api/routers/test.js

test.js

const express = require('express');
const router = express.Router();

router.get('/test', (req, res) => {
    res.json({ message: 'Hello from the backend!' });
});

module.exports = router;

设置.env文件
.env

PORT=3000
NODE_ENV=development

创建nodemon.json文件

{
  "watch": ["api/**/*.js"],
  "ext": "js,json",
  "ignore": ["node_modules/**", "frontend/**"],
  "exec": "node api/index.js",
  "env": {
    "NODE_ENV": "development"
  },
  "restartable": "rs",
  "colours": true,
  "verbose": false,
  "delay": 1000
}

运行项目

npm run start

访问http://localhost:3000/api/v1/test

{
  "message": "Hello from the backend!"
}

编辑vercel.json

{
  "version": 2,
  "builds": [
    {
      "src": "api/index.js",
      "use": "@vercel/node"
    },
    {
      "src": "frontend/package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "dist"
      }
    }
  ],
  "routes": [
    {
      "src": "/api/(.*)",
      "dest": "/api/index.js"
    },
    {
      "handle": "filesystem"
    },
    {
      "src": "/(.*)",
      "dest": "/frontend/$1"
    }
  ]
}

添加supabase环境变量

.env

SUPABASE_URL=your-supabase-url
SUPABASE_ANON_KEY=your-supabase-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key

在backend中创建config文件

# pwd => backend
mkdir api/config
touch api/config/supabase_conifig.js

supabase_config.js

const { createClient } = require('@supabase/supabase-js');
require('dotenv').config();

// Supabase configuration
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!supabaseUrl || !supabaseAnonKey) {
  console.warn('⚠️  Supabase credentials not found in environment variables');
  console.warn('Please set SUPABASE_URL and SUPABASE_ANON_KEY in your .env file');
}

// Create Supabase client for public operations (with RLS)
const supabase = createClient(supabaseUrl, supabaseAnonKey);

// Create Supabase admin client (bypasses RLS - use with caution)
const supabaseAdmin = supabaseServiceKey 
  ? createClient(supabaseUrl, supabaseServiceKey)
  : null;

module.exports = {
  supabase,
  supabaseAdmin
};

创建一个测试文件test_supabase.js

# pwd=> backend
touch test_supabase.js

test_supabase.js

const { supabase, supabaseAdmin } = require('./api/config/supabase_conifig');

async function testSupabase() {
  try {
    // Test public read
    const { data, error } = await supabase.from('test_table').select('*');
    if (error) throw error;
    console.log('Public read successful:', data);

    // Test admin write
    if (supabaseAdmin) {
      const { data: insertData, error: insertError } = await supabaseAdmin
        .from('test_table')
        .insert([{ name: 'Test User', email: 'test@example.com' }]);
      if (insertError) throw insertError;
      console.log('Admin write successful:', insertData);
    } else {
      console.warn('⚠️  Service role key not found - admin write test skipped');
    }
  } catch (error) {
    console.error('Supabase test failed:', error.message);
  }
}

testSupabase();