Site icon May's Notes

Express + MongoDB Atlas + Vercel 部署後端 API

1 gt7D9sVdfvyp3TR63C9 Rg

筆記非教程。

建立 Express 專案

先安裝 express 和 dotenv

npm install express dotenv

專案結構

index.js

const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

module.exports = app

package.json 中的 scripts 加上 start, server

"scripts": {
  "start": "node ./src/index.js",
  "server": "nodemon ./src/index.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

連接 MongoDB Atlas

創建 Cluster

註冊MongoDB Atlas帳號,新建 Project > Cluster (可以選M0 / Google Cloud / Taiwan)

建好後點 Connect

Driver 選 Mongoose,然後複製第三步的 connection string

Express 連接資料庫

安裝 mongoose

npm install mongoose

新增 .env

MONGODB_URI="mongodb+srv://root:<db_password>@<cluster_name>.homtu.mongodb.net/<db_name>?retryWrites=true&w=majority&appName=<cluster_name>"
PORT="3000"

connections/index.js

const mongoose = require('mongoose')
const uri = process.env.MONGODB_URI

const clientOptions = {
  serverApi: { version: '1', strict: true, deprecationErrors: true },
}

const run = async () => {
  try {
    await mongoose.connect(uri, clientOptions)
    console.log("成功連接到 MongoDB!")
  } catch (error) {
    console.error("連接 MongoDB 時出錯:", error)
    process.exit(1)
  }
}

const disconnect = async () => {
  try {
    await mongoose.disconnect()
    console.log("MongoDB 連線已關閉")
  } catch (error) {
    console.error("關閉 MongoDB 連線時出錯:", error)
  }
}

module.exports = { run, disconnect }

index.js

require('dotenv').config()
const express = require('express')
const app = express()
const cors = require('cors')
const db = require('./connections')

db.run().catch(console.dir)

app.use(cors())
app.use(express.json())

app.get('/', (req, res) => {
  res.send('Hello World!')
})

const startServer = async () => {
  try {
    await db.run()
    console.log('資料庫連接成功')

    const port = process.env.PORT || 3000
    app.listen(port, () => {
      console.log(`伺服器正在監聽 http://localhost:${port}`)
    })
  } catch (error) {
    console.error('啟動伺服器時出錯:', error)
    process.exit(1)
  }
}

startServer()

process.on('SIGINT', async () => {
  await db.disconnect()
  console.log('伺服器關閉,MongoDB 連線已斷開')
  process.exit(0)
})

module.exports = app

資料的CRUD

在 src 底下新增 routes, models 資料夾

models

以 User 為例

// models/User.js
const mongoose = require('mongoose')

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
}, { timestamps: true })

const User = mongoose.model('User', userSchema)

module.exports = User

註:Mongoose 會根據 model 名稱自動推測 collection 名稱,並將其轉換為小寫複數形式,因此 collection 名稱為 users

routes

還可以把處理邏輯再拆分到 services 中,這邊就不展開了

// routes/user.js
const express = require('express')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const User = require('../models/User')
const router = express.Router()

// 註冊新用戶
router.post('/register', async (req, res) => {
  const { name, email, password } = req.body
  try {
    // 檢查是否已有此電子郵件的用戶
    const existingUser = await User.findOne({ email })
    if (existingUser) {
      return res.status(400).json({ error: 'Email already exists' })
    }

    // 密碼加密
    const hashedPassword = await bcrypt.hash(password, 10)

    // 創建新用戶
    const newUser = new User({
      name,
      email,
      password: hashedPassword,
    })

    await newUser.save()
    res.status(201).json({ message: 'User created successfully' })
  } catch (error) {
    res.status(500).json({ error: 'Server error' })
  }
})

// 用戶登入 (生成 JWT)
router.post('/login', async (req, res) => {
  const { email, password } = req.body
  try {
    const user = await User.findOne({ email })
    if (!user) {
      return res.status(400).json({ error: 'Invalid credentials' })
    }

    // 密碼比對
    const match = await bcrypt.compare(password, user.password)
    if (!match) {
      return res.status(400).json({ error: 'Invalid credentials' })
    }

    // 生成 JWT
    const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' })
    res.json({ token })
  } catch (error) {
    res.status(500).json({ error: 'Server error' })
  }
})

// 獲取用戶資料
router.get('/:id', async (req, res) => {
  try {
    const user = await User.findById(req.params.id)
    if (!user) {
      return res.status(404).json({ error: 'User not found' })
    }
    res.json(user)
  } catch (error) {
    res.status(500).json({ error: 'Server error' })
  }
})

// 更新用戶資料
router.put('/:id', async (req, res) => {
  const { name, email, password } = req.body
  try {
    const user = await User.findById(req.params.id)
    if (!user) {
      return res.status(404).json({ error: 'User not found' })
    }

    // 更新字段
    if (name) user.name = name
    if (email) user.email = email
    if (password) user.password = await bcrypt.hash(password, 10)

    await user.save()
    res.json({ message: 'User updated successfully' })
  } catch (error) {
    res.status(500).json({ error: 'Server error' })
  }
})

// 刪除用戶
router.delete('/:id', async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id)
    if (!user) {
      return res.status(404).json({ error: 'User not found' })
    }
    res.json({ message: 'User deleted successfully' })
  } catch (error) {
    res.status(500).json({ error: 'Server error' })
  }
})

module.exports = router

在 index.js 中使用路由

// index.js
app.use(express.json())

const userRoutes = require('./routes/user')
app.use('/api/users', userRoutes)

請求時進行權限驗證

新增 middlewares/auth.js

需自行新增 JWT_SECRET 到 .env

// middlewares/auth.js
const jwt = require('jsonwebtoken')

const authenticate = (req, res, next) => {
  const token = req.header('Authorization')?.replace('Bearer ', '')
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' })
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    req.user = decoded
    next()
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' })
  }
}

module.exports = authenticate

使用 middlewares

// index.js
const userRoutes = require('./routes/user')
const authenticate = require('./middlewares/auth')

app.use('/api/users', authenticate, userRoutes)
app.use('/api/users/register', userRoutes.register)
app.use('/api/users/login', userRoutes.login)

部署 API 到 Vercel

1.新增 vercel.json 到根目錄

{
  "version": 2,
  "builds": [
    {
      "src": "/src/index.js",
      "use": "@vercel/node"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/src/index.js"
    }
  ]
}

2.將專案推上 Github
3.登入 Vercel 並且創建新的 Project
4.創建專案時選擇 Github repo 並且設置 env
5.部署完成後就能獲取 API 地址 https://<project_name>.vercel.app/

錯誤:連接 MongoDB 時出錯

如果出現下方錯誤代表需要將 IP 加到 atlas cluster 的 IP 白名單中

連接 MongoDB 時出錯: MongooseServerSelectionError: Could not connect to any servers in your MongoDB Atlas cluster. One common reason is that you're trying to access the database from an IP that isn't whitelisted. Make sure your current IP address is on your Atlas cluster's IP whitelist: https://www.mongodb.com/docs/atlas/security-whitelist/

Project > Data Services > SECURITY – Network Access > IP Access List 添加 IP

由於 Vercel 部署使用動態 IP 無法獲取一個固定 IP,所以需要新增 0.0.0.0/0 (允許所有 IP)。

Exit mobile version