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}`)
})

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 = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  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)
})

資料的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

// 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/

Exit mobile version