Skip to content

慣例快速參考

精簡參考資料 — 以表格和清單呈現,非散文說明。詳細說明請參閱 README.mddocs/conventions.txt


命名

對象慣例範例
程式碼變數camelCasefirstNameisActive
Feature 資料夾(app/單數 PascalCaseOrderUserSession
Controllers複數users.jsorders.js
Actions單數,遵循下方公式V1LoginV1CreateByAdmin
Tasks與 action 相同,加上 Task 後綴V1ExportTaskByAdmin
DB 資料表名稱複數 PascalCaseUsersUserOrders
DB 欄位名稱camelCasefirstNamecreatedAt
DB ENUM 型別名稱全大寫,無空格/底線/連字號ACTIVEPENDINGPAYMENT
FK 欄位<實體>IduserIduserOrderId
同一資料表的多個 FK加上角色前綴hostUserIdbookerUserId
第三方廠商 ID廠商前綴stripeIdtwilioId
索引名稱{Table}_{col}_{idx|unique}Users_email_unique
布林欄位連接動詞前綴isActivehasPasswordcanInvitedoesRequireApproval
常數UPPER_CASE 加底線ERROR_CODESLOCALE
資料夾/檔案名稱小寫,允許連字號my-helper.jsauth-migration.md
API route URL小寫,無分隔符號/v1/users/logoutall/v1/orders/updateemail
頁面 URL小寫加連字號/sign-in/user-profile
翻譯鍵值NAMESPACE[snake_case_key]GLOBAL[unauthorized]USER[profile_not_found]
Socket 房間ALL_CAPS_WITH_UNDERSCORESUSERCONVERSATION
Socket 事件ALL_CAPS_WITH_UNDERSCORES,FEATURE_ACTION 格式MESSAGE_CREATED

Action 與 Task 命名公式

V{version}{ActionName}[By{Role}][On{Device}]
區段規則
V{version}永遠在最前面。API 版本號。
{ActionName}PascalCase。描述其功能。
By{Role}僅當行為因角色而有實質差異時使用。
On{Device}僅當行為因平台而有實質差異時使用。

裝置後綴: OniOSOnAndroidOnMobileOnWeb — 若所有平台行為相同則省略。

範例含義
V1Login所有角色,所有裝置
V1CreateByAdmin僅限管理員,所有裝置
V1UpdateByUser僅限使用者,所有裝置
V1ExportTaskByAdmin背景工作,僅限管理員
V1UpdateByAdminOnMobile僅限管理員,iOS + Android
V1SyncTaskByUserOniOS背景工作,僅限使用者,僅限 iOS

Actions = 即時請求,立即返回回應。 Tasks = 背景工作(worker 程序)。永遠加上 Task 後綴。


HTTP 規則

規則詳情
方法僅使用 POST 和 GET — 禁止 PUT、PATCH、DELETE
POST凡是傳送資料或改變狀態,預設使用 POST
GET僅當參數可以乾淨地放在 URL query string 時使用
請求參數永遠使用 req.args — 禁止直接使用 req.bodyreq.query
URL 參數req.params 僅用於動態路徑區段(例如第三方 webhook)

回應格式

永遠是扁平結構 — 不使用 data 巢狀包裝。

js
return {
  status: 200,
  success: true,
  user: { ... },   // 以回傳內容命名
  token: '...'     // 其他欄位在同一層級
};
狀態碼使用時機
200預設(讀取、更新、查詢、登入)
201Action 建立新資源/DB 記錄
202Action 將工作移交背景工作

錯誤規則

情境錯誤處理方式
HTTP actionsreturn errorResponse(req, ERROR_CODES.X, status)
Tasks 與 socket 呼叫的 actionsthrow new Error(ERROR_CODES.X)
500 錯誤永遠不要手動返回 — 讓 middleware/error.js 處理

產生器規則

規則指令
建立 feature 骨架yarn gen Feature
建立 action 骨架yarn gen Feature -a V1Action
建立 task 骨架yarn gen Feature -t V1Task
建立 mailer 骨架yarn gen Feature -m Mailer
移除骨架yarn del Feature -a V1Example(禁止使用 rm
移除 task 骨架yarn del Feature -t V1ExampleTask

禁止手動建立 feature 檔案。禁止對產生的檔案使用 rmyarn del 會從 actions/index.js / tasks/index.js 中移除對應的匯出項目。rm 不會 — 它會留下一個損壞的匯出。 完成骨架產生後,立即使用 yarn del 刪除產生器的佔位符檔案。


JS 檔案結構(每個 .js 檔案,由上至下)

1.  標頭注釋         — 描述此檔案的用途及其方法
2.  'use strict';
3.  ENV 變數         — 從 process.env 解構
4.  Node 內建模組    — fs、path、crypto 等
5.  第三方模組       — lodash、joi、moment-timezone 等
6.  Services         — 來自 services/(包含 services/queue.js)
7.  Helpers          — 來自 helpers/、constants、feature 的 helper.js
8.  Models           — const models = require('.../models')
9.  Queues           — queue.get('XQueue') 實例(緊接在 models 之後)
10. 模組層級常數     — 可選
11. module.exports   — 在方法定義之前
12. 方法定義

每個區段內的匯入順序: 按行長度遞增。先是純整模組 require,再是解構式 require,每個子群組也按行長度遞增排序。

js
// 第三方 — 先純 require(按長度),再解構(按長度)
const _ = require('lodash');
const joi = require('joi');
const moment = require('moment-timezone');

// services
const lang = require('../../../services/language');
const queue = require('../../../services/queue');
const { ERROR_CODES, errorResponse, joiErrorsMessage } = require('../../../services/error');

模組匯出慣例

module.exports 放在方法定義之前(方法透過函式宣告提升)。

js
// ... requires、模組層級常數 ...

module.exports = { myFunction, anotherFunction };

function myFunction() { ... } // END myFunction
function anotherFunction() { ... } // END anotherFunction

函式結束注釋

每個具名函式以 // END functionName 結尾。

js
function V1Login(req, res) {
  ...
} // END V1Login

適用於所有函式:actions、tasks、helpers、services、middleware。


注釋標頭

Action 標頭:

js
/**
 * <描述>
 *
 * POST <URL>
 *
 * Must be logged in | Must be logged out | Can be both
 * Roles: ['admin']
 *
 * req.args = {
 *   email    - (STRING - REQUIRED): 使用者的電子郵件
 *   password - (STRING - REQUIRED): 未雜湊的密碼
 *   age      - (NUMBER - OPTIONAL): 使用者的年齡
 * }
 *
 * Success: Return logged in user.
 * Errors:
 *   400: Login failed. Incorrect email and/or password.
 *   401: Please confirm your email to log in.
 */

一般函式/方法標頭:

js
/**
 * <描述>
 *
 * @arg1 - (NUMBER - REQUIRED) [DEFAULT - 100]: <描述>
 * @arg2 - (NUMBER - OPTIONAL) [DEFAULT - 100]: <描述>
 *
 * return: true / false or <{ OBJECT }>
 */

禁止魔術字串

任何在多個地方使用的字串常量(狀態、型別、角色、語言、enum 值)統一放在 helpers/constants.js 中,並以名稱引用。

js
// 錯誤
if (user.locale === 'en') { ... }

// 正確
if (user.locale === LOCALE.EN) { ... }

例外: migration 保持字面值(歷史快照,不重構)。改為加上 // = SOME.CONSTANT 注釋而非匯入。

引入新的 enum 值時使用 add-constant skill。


外鍵與所有權規則

將擁有者實體的 FK 帶到每一個後代資料表,不只是直接父層。

User -> UserOrder -> UserOrderItem

UserOrder.userId          -> Users.id
UserOrderItem.userOrderId -> UserOrders.id
UserOrderItem.userId      -> Users.id   ← 此處也需要(扁平查詢 + 安全範圍限制必要)

漂移防護(必要):

  1. 在父資料表上加 UNIQUE (id, userId)
  2. 在子資料表上加複合 FK (userOrderId, userId) 參照 UserOrders(id, userId)

如此 Postgres 就會拒絕任何 userId 與父層不符的子記錄。

每個資料表的欄位順序:

  1. 主鍵 id
  2. 外鍵
  3. 第三方廠商 ID
  4. 功能特定欄位
  5. Sequelize 自動產生:deletedAtcreatedAtupdatedAt

Auth 慣例

概念詳情
Token 模型短效 access token(JWT,約 15 分鐘)+ 長效 refresh token(不透明,約 60 天)
Access tokenACCESS_TOKEN_SECRET 簽署;包含 subtypetokenVersion
Access token 傳輸回應主體;以 Authorization: jwt-<type> <token> 方式發送
Refresh token256 位元隨機不透明字串 — 永遠不存原始值,只存 SHA-256 雜湊
Refresh token 傳輸httpOnly + Secure + SameSite=strict cookie(網頁);同時也在主體(行動端)
輪換每次 /refresh 都撤銷目前 session,發行新的 session
重用偵測重放已輪換的 token → 撤銷所有使用者 sessions + 更新 tokenVersion
即時撤銷更新 <Type>.tokenVersion 使所有 access token 立即失效
Audience依型別及用戶端種類:user-webuser-appadmin-webadmin-app
用戶端種類X-Client 標頭(web|app);儲存於 session
平台X-Platform 標頭(僅供後設資料);儲存於 session;絕不放入 token audience
新增使用者型別middleware/auth.jsAUTH_TYPES 新增項目 + Passport strategy + <Type>Sessions 資料表

Socket 慣例

規則詳情
永遠使用 getIO()禁止直接匯入 io — 在 require 時它是 null
commit 後才 emit永遠在 t.commit() 之後才 emit,不在之前
房間命名ALL_CAPS_WITH_UNDERSCORES;實例房間使用 socketWrapper(id)
事件命名ALL_CAPS_WITH_UNDERSCORES,格式為 FEATURE_ACTION
循環依賴socket.js 匯入的 actions 使用 context 物件模式,而非 getIO()
測試永遠不要用 if (NODE_ENV !== 'test') 保護 emit — 使用 jest.spyOn mock

翻譯鍵值慣例

NAMESPACE[key_name]
部分規則
NAMESPACE全大寫。Feature 名稱,或共用字串使用 GLOBAL
key_name全小寫加底線 — 禁止 camelCase,禁止連字號

全域字串:languages/en.js。Feature 字串:app/FeatureName/languages/en.js。 由 yarn lang 編譯至 locales/en.json禁止直接編輯 locales/*.json


helpers/schemas.js 規則

規則詳情
命名每個匯出以 Schema 結尾
放置原則僅當用於 2 個以上 actions 時才新增;單次使用保持 inline
不加 required/optionalSchemas 是函式;呼叫端在呼叫時串接 .required().optional()
組合透過 .keys() 或直接嵌入至 action 層級的 schemas

測試檔案位置

原始檔案測試檔案
app/<Feature>/actions/V1Action.jsapp/<Feature>/tests/integration/V1Action.test.js
app/<Feature>/tasks/V1Task.jsapp/<Feature>/tests/tasks/V1Task.test.js
app/<Feature>/helper.jsapp/<Feature>/tests/helper.test.js
helpers/<name>.jstest/helpers/<name>.test.js
services/<name>.jstest/services/<name>.test.js

禁止直接把測試放在 test/ 根目錄。


套件版本管理

永遠安裝精確版本 — 不使用 ~^ 範圍。

sh
yarn add <module> --exact          # 一般依賴
yarn add <module> --exact --dev    # 開發依賴

Semver 參考:MAJOR.MINOR.PATCH(例如 v2.3.2)。


常用變數名稱

變數含義
err來自 await 搭配 .catch() 的錯誤(不在 try/catch 內)
errortry/catch 區塊中捕獲的錯誤
reqExpress request
resExpress response
result一般結果變數
args參數

Model 規則(快速摘要)

規則詳情
主鍵UUID v7
軟刪除paranoid: true;使用 scope(null) 略過
FK 索引永遠為外鍵欄位建立索引
具名索引{Table}_{col}_{idx|unique},model 和 migration 中都要有
一種使用者型別 = 一張資料表禁止單一資料表搭配角色欄位