---
# System prepended metadata

title: Nuxt + Prisma + PostgrelSQL 全端專案

---

---
title: Nuxt + Prisma + PostgrelSQL 全端專案

---

# Nuxt + Prisma + PostgrelSQL 全端專案

本專案是一個以 Nuxt 3 為前端、Prisma + PostgreSQL 為後端的電商網站，並整合 TailwindCSS、Pinia 狀態管理與 JWT 身份驗證，實現完整的會員註冊、商品購物、訂單流程與後台管理功能。

---

## 🔧 專案架構

- **MyShop/**
- frontend/ # 使用 Vite + Vue3 建構的購物網站前台
-  admin/ # 使用 Nuxt3 開發的後台管理系統
-  prisma/ # Prisma schema 與 migration 設定
-  .env # 資料庫連線設定

---

## 🖥️ 前端（Frontend）

- **框架**：Vue 3 + Vite
- **狀態管理**：Pinia（含購物車狀態持久化）
- **路由**：Vue Router
- **樣式**：Tailwind CSS + Element Plus（UI 套件）
- **功能**：
  - 商品列表與詳細頁面
  - 加入購物車與移除功能
  - 結帳功能，含庫存數量驗證
  - 訂單送出後導向訂單詳情頁

---

## 🛠️ 後台（Admin）

- **框架**：Nuxt 3
- **UI 套件**：Element Plus + Tailwind CSS
- **資料存取**：透過 API 與 Prisma 操作 PostgreSQL 資料庫
- **功能模組**：
  - 商品管理（新增 / 編輯 / 刪除 / 庫存設定）
  - 訂單管理（查看訂單、訂單詳情）
  - 管理員登入 / 登出
  - 管理員管理 (新增、編輯)

---

## 🗄️ 資料庫

- **類型**：PostgreSQL（部署於 Render）
- **ORM**：Prisma ORM
- **Schema 架構**：
  - `Product`：商品資訊與庫存數量
  - `Order`：訂單總覽資料
  - `OrderItem`：訂單中的每一筆商品與數量明細
  - `User`: 會員
  - `Admin`: 管理員
  

---

# 🌐 Nuxt 是什麼？
Nuxt.js（通常簡稱為 Nuxt）是一個基於 Vue.js 的高階框架，用來構建 現代化 Web 應用程式。它幫助開發者更容易地實作：

1. 伺服器端渲染（SSR）

2. 靜態網站生成（SSG）

3. 單頁應用（SPA）

4. 全端應用（Fullstack）





## 💡 核心特點


#### 功能	說明
✅ SSR（Server-Side Rendering）	頁面由伺服器預先渲染，對 SEO 友好，載入速度快

✅ 靜態網站生成（SSG）	預先生成 HTML，可部署到 CDN 上，適合部落格、官網等

✅ 自動路由	pages/ 資料夾自動變成對應的路由

✅ 全端支援	可寫 API：server/api/*.ts 是後端邏輯，整合 Prisma、DB 很方便

✅ 強化 DX（開發體驗）	Hot reload、TypeScript 支援、Composable API（setup 語法）

✅ 模組系統	如 auth、i18n、tailwind、pinia、image 等模組一行裝起


## 🛠 Nuxt 的用途
### 1. 建立 SEO 友善的網站
像是： 部落格、電商網站、公司介紹頁面。

因為 SSR 可以讓搜尋引擎更容易抓到內容。

### 2. 開發 Fullstack 應用
Nuxt 3 提供 server API 支援，例如你可以使用：

/server/api 目錄撰寫後端 API

搭配 Prisma 使用資料庫

使用 JWT 驗證登入系統

👉 一個 Nuxt 專案就可以包辦前後端，非常適合開發管理後台、會員系統、電商平台等。

### 3. 建立靜態網站（預渲染）

適合一次編譯後部署上 CDN，像是： 部落格、產品介紹頁、 技術文件

使用 `nuxt generate` 指令即可。

### 4. 做為 Vue 應用的升級版

你如果本來會寫 Vue，只要學 Nuxt 的額外功能，馬上能上手：

頁面路由 = pages 資料夾

中介層（middleware）

插件系統（plugins）

狀態管理（Pinia）

API server（server/api）

---

## Nuxt 畫面架構說明
畫面的整體結構主要由 Header、Sidebar 與 MainContent 三大區塊組成。


![截圖 2025-04-23 下午2.08.07](https://hackmd.io/_uploads/H1XNSW8kex.png)


在這個結構中，**會根據路由變化的部分僅有 MainContent 區塊。**

在 Nuxt 中，我們可以透過頁面加入以下程式碼，來指定該頁面所使用的 Layout 以及 Middleware：

```
definePageMeta({
  layout: 'admin',
  middleware: ['auth']
});
```


加入上述程式碼後，該頁面就會預設使用 `admin` 這個 Layout，並且套用名為 `auth` 的中介層驗證機制。

--- 

### Layout 插槽機制

如下圖，`<slot />` 代表的是可替換內容的插槽，Nuxt 會根據當前的路由，自動渲染對應頁面的內容到這個位置：


![截圖 2025-04-23 下午2.21.05](https://hackmd.io/_uploads/Hk2-D-Lkgl.png)


這樣的架構讓開發者能夠有效地拆分畫面、重用結構，並在不同頁面中注入各自的內容，維持一致的 UI 外框設計。



---


### 全域管理(Pinia + persist)

在Vue當中會使用**Pinia**來做全域狀態管理，搭配 **persist**插件，把需要的狀態儲存到**localstorge**。

在Nuxt中當然也可以使用。只是有些設定上的差異性。


### Vue
main.js
```
const pinia = createPinia();

pinia.use(piniaPluginPersistedstate);
app.use(pinia);
```

### Nuxt

nuxt.config.ts

```
import { resolve } from 'path';
export default defineNuxtConfig({
  modules: [
  '@nuxtjs/tailwindcss', 
  '@pinia/nuxt', 
  'pinia-plugin-persistedstate/nuxt'
  ]
}); 
```

plugins/piniaPersist.client.ts


```
import { defineNuxtPlugin } from '#app'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import type { Pinia } from 'pinia'

export default defineNuxtPlugin((nuxtApp) => {
  (nuxtApp.$pinia as Pinia).use(piniaPluginPersistedstate)
})

```



---
在Nuxt中使用**pinia**，需要注意的事

一般來說Vue中的template會類似下面圖示:

![截圖 2025-04-23 下午3.42.00](https://hackmd.io/_uploads/ryJQcMLJel.png)


但是在Nuxt中會出下面圖示中的錯誤訊息， 

**Hydration completed but contains mismatches.**

![截圖 2025-04-23 下午3.36.59](https://hackmd.io/_uploads/HJ6zFMUygg.png)

這個主要的原因是， Nuxt (或 Vue SSR) 在執行「Hydration」（將伺服器端渲染的 HTML 轉換為可互動的 Vue 組件）時，發現 伺服器端渲染出來的內容和客戶端接手的內容不一致。

###  為什麼會出現？

#### 1. 使用了 localStorage / window 等 browser-only API

在下面圖中，**currentAdmin**是 藉由pinia persist 自動從 localstorage存取的。

所以才導致 **Hydration completed but contains mismatches.** 出現這個錯誤訊息。
![截圖 2025-04-23 下午3.45.38](https://hackmd.io/_uploads/HkWThMIkxg.png)


![截圖 2025-04-23 下午3.45.55](https://hackmd.io/_uploads/Sk9TnfUJge.png)

### 解決方法 

#### 使用  <ClientOnly></ClientOnly> 包裹不需要SSR的部分。

![截圖 2025-04-23 下午3.59.42](https://hackmd.io/_uploads/SyXW0fUJgx.png)

### 💬 為什麼這樣就可以？
因為 **<ClientOnly>** 會跳過 SSR，那 SSR 階段根本不會產生這段 HTML，自然也就沒有 hydration 的錯誤。Vue 會在 CSR 階段動態載入它
     
    
---    
    
## Middleware    
    
#### 是一種在頁面渲染之前攔截請求並執行邏輯的機制，通常用來實作以下幾種功能：   
    
![截圖 2025-04-23 下午5.34.51](https://hackmd.io/_uploads/B16r4VLJlg.png)

    
#### 在這個專案中，我們使用了兩種類型的 Middleware，分別為：

- admin/middleware：屬於 Nuxt 的前端（client-side 或 universal）middleware，主要用於 路由守衛與驗證登入狀態。

- admin/server/middleware：屬於 Nuxt 的後端 Middleware（基於 Nitro server），用於處理例如 CORS 設定、Header 檢查、Token 驗證等伺服器層級邏輯。

如下圖所示，兩者在執行階段與角色上的差異一目了然：
![截圖 2025-04-23 下午5.36.06](https://hackmd.io/_uploads/HyafS481eg.png)
    
    
#### 程式碼範例參考：
    
**admin/middleware/auth.ts（前端路由驗證 Middleware）**
    
```
import { useAuthStore } from '~/store/auth';

export default defineNuxtRouteMiddleware(() => {
  if (process.server) return;

  const authStore = useAuthStore();

  if (!authStore.token || !authStore.admin) {
    return navigateTo('/login');
  }
});

``` 
    
**admin/server/middleware/cors.ts（伺服器端 Middleware）**
```
export default defineEventHandler((event) => {
  setHeader(event, 'Access-Control-Allow-Origin', '*') 
  setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization')

  // 處理預檢請求
  if (getMethod(event) === 'OPTIONS') {
    return new Response(null, { status: 204 })
  }
})
```    
    
 

---
   
    
## Nuxt 中的 API 運作方式
    
Nuxt 3 採用了 **Nitro Server** 作為內建的後端伺服器架構，讓你可以 直接在 server/api/ 目錄下撰寫 API，無需額外設定 Express 或其他後端框架。

✅ 支援 REST / JSON / server functions。

✅ 可以存取 cookies、headers、query、body 等。

✅ 適合建構 SSR-friendly 的全端應用。   
    
    
## 匹配 HTTP 請求方法 (HTTP Request Method)**
    
我們可以添加 .get、.post、.put 或 .delete 等檔案名稱後綴，來匹配對應的 HTTP request methods。   
    
- /api/product/index.**get**.ts ---> 取得所有商品資料    
- /api/product/index.**post**.ts ---> 新增商品資訊 
- /api/product/[id].**get**.ts ---> 取得單一筆商品資訊
- /api/product/[id].**put**.ts ---> 更新單一筆商品資訊
- /api/product/[id].**delete**.ts ---> 刪除單一筆商品資訊    
    
#### 建立第一個測試API
    
Nuxt 會自動掃描 server 目錄中的檔案結構，建立 Server API 時通常以 .js 或 .ts 作為副檔名，並依照官方建議，每個檔案都應該預設匯出 **defineEventHandler()** 函數，並在其 handler 內實作處理邏輯。
    
**先在 server/api 底下建立一個hello.ts**
    
```
export default defineEventHandler(() => {
  return { msg: 'Hello, Nuxt API!' };
});
```

訪問http://localhost:3000/api， 會看見回傳的 JSON 資料。
    
![截圖 2025-04-24 上午10.22.36](https://hackmd.io/_uploads/rkcv-XDyel.png)
    
    
使用VScode的插件**Thunder Client**測試POST Request，成功的話會出現 200 OK
    
![截圖 2025-04-24 上午10.53.39](https://hackmd.io/_uploads/HJftumPJlg.png)
    
    
    

    
    
#  Prisma 是什麼？
Prisma 是一套現代化的 TypeScript ORM（Object-Relational Mapping）工具，可以讓你更簡潔、安全地與資料庫互動。
它支援多種資料庫，包括 PostgreSQL、MySQL、SQLite、SQL Server 等。

✅ Prisma = 強型別 + 自動補全 + SQL 抽象化 + DB Schema 管理工具！  
    
    
## 🎯 Prisma 的用途
✅ 對資料庫進行 CRUD 操作（Create / Read / Update / Delete）

✅ 使用 TypeScript 提供自動補全與型別檢查

✅ 管理資料庫 Schema（使用 Prisma Migrate）

✅ 與 REST API / GraphQL 無縫整合

✅ 提供 DB client，不需手寫 SQL
    
## 🧠 Prisma 的運作流程
    
### Prisma 的基本流程分成 3 個部分：

1️⃣ Schema 定義（prisma/schema.prisma）
你用類似 SQL 的語法定義資料表與關聯：    
    
```
model Product {
  id          Int       @id @default(autoincrement())
  name        String
  description String
  price       Float
  stock       Int
  orderItems  OrderItem[]
  createdAt   DateTime  @default(now())
}
```    
    
### Prisma Client 自動產生
執行以下指令來產生 Prisma Client：
    
```
npx prisma generate
```
    
Prisma 會根據你定義的 schema 自動生成可用的 TypeScript 資料庫操作方法：
    
```
const users = await prisma.product.findMany();
```    
    
你會得到：

- 型別保護（不會打錯欄位）

- 自動補全（更快開發）

- 避免 SQL injection    
    
    
3️⃣ Migrate 管理資料庫
使用 npx prisma migrate 自動同步 Prisma schema 到你的資料庫：

```
npx prisma migrate dev --name init
```

這會產生：

- 一份 migration SQL 檔案
    ![截圖 2025-04-24 上午11.37.41](https://hackmd.io/_uploads/HJwzzEDJxx.png)
- 自動更新資料庫結構
- 確保團隊協作時 schema 一致

    
##  常用 CRUD 操作指令（以 product 為例）
###  1. 取得單筆資料（findUnique）    
    
```
    const product = await prisma.product.findUnique({ where: { id } });
```    

###  2. 取得多筆資料（findMany）    
```
   const product = await prisma.product.findMany();
```
    
###  3. 條件搜尋（帶過濾條件）

```
    const product = await prisma.product.findMany({
      where: {   
        price: 10
      },
    });
```  
    
### 4. 新增資料（create）
    
```
    const product = await prisma.product.create({
      data: {
        name: body.name,
        price: body.price,
        stock: body.stock,
        description: body.description || ''
      }
    });
```    
    
### 5. 修改資料（update）
    
```
    const updated = await prisma.product.update({
      where: { id },
      data: {
        name: body.name,
        price: body.price,
        stock: body.stock,
        description: body.description 
      }
    });
```    
    
### 6. 刪除資料（delete）
    
```
    
prisma.product.delete({ where: { id } })
    
```      
    
## 部署之前確認完成執行下面程式碼

```
npx prisma migrate dev --name xxx
```    
    
```
npx prisma migrate deploy
```    
    
最後確認 migration 狀態

```
npx prisma migrate status
```    
    
最後應該會顯示
```
1 migration found in prisma/migrations

Database schema is up to date!
```
    
# 部署到Vercel
         
    
#### 1. 註冊Vercel，直接使用GitHub登入

#### 2.新增部署專案，點擊**Add New**，選擇「 Project 」  
    
![截圖 2025-04-25 下午1.43.59](https://hackmd.io/_uploads/HyRIWjdyge.png)
    
#### 3.選擇GitHub倉庫中，你要部署的專案。點擊 「 Import 」 
    
![截圖 2025-04-25 下午1.46.52](https://hackmd.io/_uploads/B1b9GoOJle.png)
   
    
#### 4.進入畫面後，可以自訂Project Name，會根據你的 Project Name產生對應的網址。    
![截圖 2025-04-25 下午1.50.28](https://hackmd.io/_uploads/ByenMsO1xx.png)

如果你的專案中有2個子專案要分別部署的話，點擊Edit選擇目前想要部署的專案。

![截圖 2025-04-25 下午1.52.26](https://hackmd.io/_uploads/HJvhQo_Jlg.png)

點擊後可以看到子專案有哪些，選擇你要的子專案。Vercel會直接辨識你的專案類型。 
    
    
![截圖 2025-04-25 下午1.57.20](https://hackmd.io/_uploads/BJAv4od1le.png)
    

最後按**Deploy**，讓專案開始部署吧！
    
![截圖 2025-04-25 下午1.58.40](https://hackmd.io/_uploads/SJ_lBi_1eg.png)

 
部署成功後，你可以在 Vercel 專案的 Domains 區塊中看到對應的網址。
接下來就可以把這個網址分享給其他人，讓大家可以瀏覽你完成的網站囉！

如果你有在這個專案中部署 API，那這個網址也會成為你 API 的對外路徑，記得在前端或其他應用中使用這個網址來呼叫 API。    
    
## Vercel的環境變數設定  

### 點擊Setting >   Environment Variables  
    
![截圖 2025-04-25 下午2.06.03](https://hackmd.io/_uploads/BkpPuodyex.png)

    
### 新增環境變數
    
直接在下面圖中紅色框框輸入你的環境變數Key和Value，輸入後按下Save。
    
![截圖 2025-04-25 下午2.08.38](https://hackmd.io/_uploads/BJv5viuJlg.png)
    
然後往下滑動，會看到你剛剛新增的環境變數。
    
    
![截圖 2025-04-25 下午2.06.42](https://hackmd.io/_uploads/SJKMOsdyee.png)
    
## 部署Vercel可能會發生的錯誤
    
### 子專案中的Frontend，只是純靜態部署。
    
必須要在專案底下建立一個 vercel.json 來處理 rewrites
    
```
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/" }
  ]
}
```   
    
這樣任何未知的路徑都會 fallback 到 /index.html，由前端框架接管路由。
    
否則無法正確處理「非根目錄」的路由，導致 **404 NOT_FOUND**。   
    
### 如果是使用Nuxt SRR的話，確認nuxt.config.ts 有設定以下的程式碼
    
```
export default defineNuxtConfig({
  ssr: true,
  nitro: {
    preset: 'vercel'
  }
});
```    
   
# 參考資料   
[Nuxt 3 Server API 與 Nitro Engine]( https://ithelp.ithome.com.tw/articles/10301197)
    
[Nuxt](https://nuxt.com/docs/api)
    
[pinia](https://pinia.vuejs.org/ssr/nuxt.html)
    
[pinia-Persistedstate](https://prazdevs.github.io/pinia-plugin-persistedstate/zh/frameworks/nuxt.html)
    
# GitHub
    
[Shop](https://github.com/larryzeng123/larryShop) 

# 專案網站
    
[前台購物頁面](https://larry-shop-frontend-tawny.vercel.app/)    
[後台管理頁面](https://larry-shop-mauve.vercel.app/)  
[GitHub](https://github.com/Larryz1215/larryShop)    