【App 工程師的前端轉生術 03】資料層的防護罩:用 Zod 與 Repository 打造原生級安全感
01. 前言:為什麼 console.log 裡全是 undefined?
在上一篇,我們建立了對 Next.js Runtime 的認知。今天我們要深入 「資料層(Data Layer)」。
作為 iOS/Android 開發者,我們對「資料解析」這件事是非常嚴肅的。
- iOS: 你會寫
struct User: Codable,如果 JSON 欄位對不上,JSONDecoder會直接拋出 Error,你絕不會拿到一個爛掉的 Model。 - Android: 你會用
data class User(...)搭配 Moshi 或 Kotlin Serialization,確保 Null Safety 在解析時就生效。
但在前端,TypeScript 的型別檢查只存在於 「編譯期(Compile Time)」。
當你的 API 回傳了錯誤的 JSON(例如後端無預警把 user_name 改成 username),TypeScript 完全不會報錯。直到你的 User 在 UI 上點擊某個按鈕,程式崩潰,噴出著名的 Cannot read properties of undefined。
這對 App 工程師來說是不可接受的技術債。今天我們要導入 Zod 與 Repository Pattern,把這種不確定性在邊界層就擋下來。
02. 定義 Model:TypeScript Interface 還不夠
在 App 開發中,我們習慣定義 Data Model。
Mobile 思維
- Android (Kotlin):
@Serializable data class User(val id: String, val name: String, val email: String?) - iOS (Swift):
struct User: Codable { let id: String let name: String let email: String? }
前端現狀與問題
你可能會直接在 TypeScript 寫:
// ❌ 這只是給編譯器看的,執行時它不存在
interface User {
id: string;
name: string;
email?: string;
}
然後直接 const user = await res.json() as User;。
這是一個謊言! as User (Type Assertion) 只是你強迫編譯器閉嘴。如果 API 回傳 { "id": 123 } (數字而非字串),你的 App 在執行時照樣會掛掉。
解法:Zod (Runtime Schema Validation)
我們需要一個東西能在「執行期」驗證資料,就像 JSONDecoder 或 Moshi 做的事。Zod 就是這個標準答案。
請先安裝:npm install zod
// src/domain/models/user.ts
import { z } from 'zod';
// 1. 定義 Schema (這是執行期要跑的程式碼)
export const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email().optional(), // 支援格式驗證
});
// 2. 導出 TypeScript Type (這是給編譯器看的)
// z.infer 會自動根據上面的 Schema 產生 interface,不用寫兩次!
export type User = z.infer<typeof UserSchema>;
現在,我們擁有了跟 Native App 一樣強壯的 Model 定義:既有靜態型別檢查,又有執行期資料驗證。
03. 資料獲取:別再 Component 裡寫 fetch
這是一個最典型的前端壞習慣:在 View (React Component) 裡面直接呼叫 API。
錯誤示範 (Anti-Pattern):
// ❌ 這是把網路層邏輯寫死在 View 裡 (Violation of SRP)
export default function UserPage() {
const [user, setUser] = useState<User>();
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data)); // 沒驗證資料就直接塞進 State
}, []);
return <div>{user?.name}</div>;
}
App 開發者視角:
這就像是你把 URLSession.shared.dataTask 寫在 SwiftUI.View.onAppear 裡,或者是把 OkHttp 請求寫在 Android 的 Compose 函數裡。這是架構上的大忌,導致難以測試且無法重用。
04. 架構設計:Repository Pattern
我們要建立一個乾淨的資料層,負責處理「資料從哪裡來」以及「資料是否合法」。
架構層級
- API Client: 封裝底層 HTTP Client (axios/fetch)。
- Repository: 定義業務邏輯需要的資料接口,並負責將 API 資料透過 Zod 轉成 Domain Model。
Step 1: 建立 API Client (類似 Retrofit / Moya)
// src/infrastructure/api/apiClient.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
export const apiClient = {
get: async (url: string) => {
const res = await fetch(`${BASE_URL}${url}`);
if (!res.ok) throw new Error(`Network error: ${res.status}`);
return res.json();
}
};
Step 2: 實作 Repository (核心邏輯)
這裡我們將 Zod 派上用場。這段程式碼對應到 Android 的 Repository Implementation 或 iOS 的 Service Class。
// src/infrastructure/repositories/userRepository.ts
import { apiClient } from '../api/apiClient';
import { User, UserSchema } from '@/domain/models/user';
// 定義介面 (Protocol / Interface)
// 這是為了以後寫單元測試時可以輕鬆 Mock
export interface IUserRepository {
getUserProfile(id: string): Promise<User>;
}
export const userRepository: IUserRepository = {
getUserProfile: async (id: string): Promise<User> => {
// 1. 獲取原始資料 (Unknown type)
const data = await apiClient.get(`/users/${id}`);
// 2. 驗證與轉換 (Parsing)
// 如果後端欄位錯了,這裡會直接噴 error,而不是讓 UI 顯示錯誤
const result = UserSchema.safeParse(data);
if (!result.success) {
// 可以在這裡送 Log 給 Sentry / Firebase Crashlytics
console.error("Data Parsing Error:", result.error);
throw new Error("Invalid User Data Structure");
}
// 3. 回傳乾淨的 Domain Model
return result.data;
}
};
App 對照表
| 概念 | Android (Kotlin) | iOS (Swift) | Next.js (TS) |
|---|---|---|---|
| Model 定義 | data class | struct | type (via Zod) |
| JSON 解析 | Moshi / Gson | Codable (JSONDecoder) | Zod Schema |
| 網路層 | Retrofit / Ktor | Alamofire / Moya | fetch / axios |
| 資料倉庫 | Repository | Service / Manager | Repository |
05. 整合:在 Next.js 中使用
現在我們的資料層已經與 UI 無關了。我們可以在 Server Component (SSR) 中直接使用它。
// app/user/[id]/page.tsx
// 這是一個 Server Component (還記得 Part 2 說的嗎?)
import { userRepository } from '@/infrastructure/repositories/userRepository';
export default async function UserPage({ params }: { params: { id: string } }) {
// 直接呼叫 Repository,就像在 Android ViewModel 呼叫 Repo 一樣
// 這裡是在伺服器端執行的,所以連 Network Latency 都很低
const user = await userRepository.getUserProfile(params.id);
return (
<main>
<h1>{user.name}</h1>
<p>{user.email}</p>
</main>
);
}
看到差異了嗎?我們的 Page Component 變得極度乾淨。
它不需要處理 fetch,不需要擔心 json 格式錯誤,它拿到的 user 物件是經過 Zod 嚴格驗證的。如果資料錯誤,錯誤會在 userRepository 拋出,並由 Next.js 的 error.tsx 捕獲(這就像是 App 的全域 Error Handler)。
06. 小結:找回強型別的安全感
在這篇設計篇中,我們解決了 App 工程師轉職前端時最大的恐懼——不可控的資料。
- Zod 是你的
Codable/Serializable,它保證了執行期的資料型別安全。 - Repository Pattern 隔離了 API 細節,讓你的 UI 層不需要知道
fetch的存在。
現在我們有了強壯的 骨架 (Next.js Structure) 和 血液 (Data Layer)。 但在前端開發中,最複雜的往往不是獲取資料,而是 管理互動狀態(比如:點擊編輯按鈕後切換 UI、表單輸入驗證、Loading 狀態切換)。
在 App 開發中,我們會用 ViewModel 來處理這些邏輯。 在下一篇 【Part 4】,我們將探討如何用 Custom Hooks 來實作 ViewModel,這將是真正釋放 React 潛力的關鍵一步。

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。
系列文章目錄
- 【App 工程師的前端轉生術 01】工欲善其事:把 VS Code 調教成你的 Xcode/Android Studio
- 【App 工程師的前端轉生術 02】觀念重塑:Next.js Runtime 與 App 生命週期
- 【App 工程師的前端轉生術 03】資料層的防護罩:用 Zod 與 Repository 打造原生級安全感 (本文)
- 【App 工程師的前端轉生術 04】邏輯層的核心:Custom Hooks 就是你的 ViewModel
- 【App 工程師的前端轉生術 05】UI 架構篇:原子化元件設計與 Slot Pattern