如果你來自 Express 的世界,你可能習慣了自由定義路由的方式。想要一個登入端點?就寫 POST /login
。需要取得用戶資料?那就 GET /getUserInfo
。每個路由都是根據功能命名,直觀且靈活。在 Spring Boot 中,你可能用 @GetMapping("/api/users/search")
來處理搜尋,用 @PostMapping("/api/users/activate")
來啟用帳號。FastAPI 讓你用 Python 的函式裝飾器定義任何你想要的路徑結構。
今天我們要探討的是 Rails 如何用完全不同的思維來設計路由系統。Rails 不是限制你的創意,而是提供了一種經過二十年驗證的模式,讓你的 API 更一致、更可預測、更容易維護。當你真正理解 REST 的資源思維後,你會發現那些看似任意的路由命名,其實都在試圖表達同一個概念:對資源的操作。
這個知識點在我們最終的 LMS 系統中無處不在。課程、章節、課時、作業、討論,這些都是資源。學生註冊課程、提交作業、發表評論,這些都是對資源的操作。RESTful 路由不只是技術規範,更是一種組織業務邏輯的思維框架。
讓我們先看看不同框架處理「使用者管理」的典型方式:
// Express 的動作導向設計
app.post('/api/createUser', createUser)
app.get('/api/getUserById/:id', getUserById)
app.put('/api/updateUser/:id', updateUser)
app.delete('/api/deleteUser/:id', deleteUser)
app.get('/api/searchUsers', searchUsers)
app.post('/api/activateUser/:id', activateUser)
// Spring Boot 的服務導向設計
@RestController
@RequestMapping("/api")
public class UserController {
@PostMapping("/users/register")
public User registerUser(@RequestBody UserDto user) {}
@GetMapping("/users/profile/{id}")
public UserProfile getUserProfile(@PathVariable Long id) {}
@PutMapping("/users/settings/{id}")
public void updateSettings(@PathVariable Long id, @RequestBody Settings settings) {}
}
這些設計沒有錯,但它們把每個操作都當作獨立的動作。Rails 選擇了不同的視角:
# Rails 的資源導向設計
Rails.application.routes.draw do
resources :users do
member do
post :activate
end
collection do
get :search
end
end
end
# 這會生成以下路由:
# GET /users # index - 列出所有使用者
# GET /users/new # new - 顯示新建表單(API 模式通常省略)
# POST /users # create - 建立新使用者
# GET /users/:id # show - 顯示特定使用者
# GET /users/:id/edit # edit - 顯示編輯表單(API 模式通常省略)
# PATCH /users/:id # update - 更新使用者
# DELETE /users/:id # destroy - 刪除使用者
# POST /users/:id/activate # activate - 啟用使用者(自定義成員動作)
# GET /users/search # search - 搜尋使用者(自定義集合動作)
Rails 對 REST 的堅持不是教條主義,而是基於實際經驗的設計決策。這個決策帶來了幾個深遠的影響:
一致性帶來的可預測性:當團隊中的每個人都知道 resources :courses
會產生什麼路由,溝通成本大幅降低。新加入的開發者不需要查文件就能猜出大部分 API 的結構。
約束促進更好的設計:當你發現很難用 REST 表達某個功能時,通常意味著你需要重新思考資源的邊界。例如,「發送郵件」這個動作,在 REST 中會變成「建立一個郵件發送任務」:
# 不好的設計:動作導向
post '/api/send_email'
# 好的設計:資源導向
# 把郵件發送當作資源
resources :email_deliveries, only: [:create, :show]
# POST /email_deliveries - 建立發送任務
# GET /email_deliveries/:id - 查詢發送狀態
狀態機的自然表達:許多業務邏輯本質上是狀態轉換。REST 的資源模型能優雅地表達這些轉換:
# 訂單狀態管理
resources :orders do
member do
patch :pay # 從 pending 轉為 paid
patch :ship # 從 paid 轉為 shipped
patch :cancel # 轉為 cancelled
end
end
在 Rails 中,最基本的路由宣告incredibly簡潔:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :courses
resources :users
resources :enrollments
end
end
end
但這簡單的宣告背後,Rails 為你做了大量的工作:
# 讓我們深入了解 resources 實際產生的內容
Rails.application.routes.draw do
resources :courses
end
# 相當於手動定義:
get '/courses', to: 'courses#index', as: :courses
get '/courses/new', to: 'courses#new', as: :new_course
post '/courses', to: 'courses#create'
get '/courses/:id', to: 'courses#show', as: :course
get '/courses/:id/edit', to: 'courses#edit', as: :edit_course
patch '/courses/:id', to: 'courses#update'
put '/courses/:id', to: 'courses#update'
delete '/courses/:id', to: 'courses#destroy'
LMS 系統中充滿了階層關係。課程包含章節,章節包含課時,課程有很多學生註冊。這些關係如何在路由中表達?
Rails.application.routes.draw do
resources :courses do
resources :chapters do
resources :lessons
end
resources :enrollments
resources :reviews
end
end
# 這會產生:
# GET /courses/:course_id/chapters
# GET /courses/:course_id/chapters/:chapter_id/lessons
# POST /courses/:course_id/enrollments
但是,過深的巢狀會讓 URL 變得冗長且難以管理。Rails 提供了 shallow routing 來解決這個問題:
Rails.application.routes.draw do
resources :courses do
resources :chapters, shallow: true do
resources :lessons, shallow: true
end
end
end
# 產生更簡潔的路由:
# GET /courses/:course_id/chapters - 列出課程的所有章節
# POST /courses/:course_id/chapters - 為課程建立新章節
# GET /chapters/:id - 顯示特定章節(不需要 course_id)
# PATCH /chapters/:id - 更新章節
# DELETE /chapters/:id - 刪除章節
# GET /chapters/:chapter_id/lessons - 列出章節的所有課時
# GET /lessons/:id - 顯示特定課時
這種設計體現了一個重要原則:建立時需要上下文,存取時可以獨立。
標準的七個動作不總是足夠的。Rails 區分了兩種自定義路由:
resources :courses do
member do
# 作用於特定資源實例
post :enroll # POST /courses/:id/enroll
post :publish # POST /courses/:id/publish
get :certificate # GET /courses/:id/certificate
end
collection do
# 作用於資源集合
get :popular # GET /courses/popular
get :recommended # GET /courses/recommended
post :import # POST /courses/import
end
end
選擇 member 還是 collection 的關鍵在於:這個動作是針對特定實例還是整個集合?
讓我們為 LMS 系統設計完整的路由結構,展示如何用 RESTful 思維組織複雜的業務邏輯:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# 使用者認證(這裡 sessions 被當作資源)
resources :sessions, only: [:create, :destroy]
resources :registrations, only: [:create]
# 使用者管理
resources :users do
member do
patch :activate
patch :deactivate
post :reset_password
end
collection do
get :instructors # 獲取所有講師
end
end
# 課程管理 - 核心資源
resources :courses do
member do
post :publish
post :archive
get :preview
end
collection do
get :featured
get :trending
end
# 巢狀資源 - 使用 shallow routing
resources :chapters, shallow: true do
member do
patch :reorder # 調整章節順序
end
resources :lessons, shallow: true do
member do
post :complete # 標記課時完成
get :transcript # 獲取影片字幕
end
# 課時的附件
resources :materials, shallow: true
end
end
# 註冊管理 - 關聯但獨立的資源
resources :enrollments, shallow: true do
member do
patch :suspend
patch :resume
get :progress # 學習進度
get :certificate # 結業證書
end
end
# 作業系統
resources :assignments, shallow: true do
resources :submissions, shallow: true do
member do
post :grade # 評分
post :return # 退回重做
end
end
end
# 討論區
resources :discussions, shallow: true do
resources :posts, shallow: true do
member do
post :upvote
post :pin
end
end
end
# 評價系統
resources :reviews, shallow: true
end
# 搜尋功能 - 特殊的資源集合
namespace :search do
resources :courses, only: [:index]
resources :users, only: [:index]
resources :discussions, only: [:index]
end
# 分析報表 - 唯讀資源
namespace :analytics do
resources :course_reports, only: [:index, :show]
resources :student_reports, only: [:index, :show]
resources :engagement_metrics, only: [:index]
end
end
end
end
有些業務邏輯看似不適合 REST,但通過重新思考,我們總能找到優雅的表達方式:
# 場景 1:批次操作
# 不好的設計
post '/api/courses/batch_update'
# 好的設計:把批次操作當作資源
resources :course_batch_operations, only: [:create, :show]
# POST /course_batch_operations - 建立批次操作
# GET /course_batch_operations/:id - 查詢操作進度
# 場景 2:複雜的搜尋
# 不好的設計
get '/api/searchCoursesWithFilters'
# 好的設計:搜尋結果作為資源
resources :course_searches, only: [:create, :show] do
member do
get :results
end
end
# POST /course_searches - 建立搜尋(保存搜尋條件)
# GET /course_searches/:id/results - 獲取搜尋結果
# 場景 3:工作流程
# 不好的設計
post '/api/submitAndApproveAssignment'
# 好的設計:分解為獨立的資源操作
resources :assignment_submissions do
member do
patch :submit # 學生提交
patch :review # 進入審核
patch :approve # 講師批准
patch :reject # 講師拒絕
end
end
Rails 提供了強大的路由約束功能,讓我們能實現複雜的路由邏輯:
Rails.application.routes.draw do
# API 版本控制
namespace :api do
# 版本 1 - 穩定版
namespace :v1 do
resources :courses
end
# 版本 2 - 實驗性功能
namespace :v2, defaults: { format: :json } do
resources :courses do
# V2 新增的 AI 功能
member do
post :generate_quiz
post :summarize
end
end
end
end
# 基於子域名的路由
constraints subdomain: 'api' do
resources :courses
end
# 基於請求格式的約束
resources :courses, constraints: { format: :json }
# 自定義約束類
class BetaUserConstraint
def self.matches?(request)
user = User.find_by(token: request.headers['Authorization'])
user&.beta_tester?
end
end
constraints BetaUserConstraint do
namespace :beta do
resources :ai_features
end
end
# 動態路由約束
resources :courses do
# 只有已發布的課程才有這些路由
constraints -> (request) { Course.find(request.params[:id]).published? } do
member do
post :enroll
get :preview
end
end
end
end
路由是 API 的合約,必須嚴格測試:
# spec/routing/courses_routing_spec.rb
require 'rails_helper'
RSpec.describe "Courses routing", type: :routing do
describe "standard RESTful routes" do
it "routes to #index" do
expect(get: "/api/v1/courses").to route_to(
controller: "api/v1/courses",
action: "index"
)
end
it "routes to #show" do
expect(get: "/api/v1/courses/1").to route_to(
controller: "api/v1/courses",
action: "show",
id: "1"
)
end
it "routes to #create" do
expect(post: "/api/v1/courses").to route_to(
controller: "api/v1/courses",
action: "create"
)
end
end
describe "custom member routes" do
it "routes to #publish" do
expect(post: "/api/v1/courses/1/publish").to route_to(
controller: "api/v1/courses",
action: "publish",
id: "1"
)
end
end
describe "nested resources" do
it "routes to chapters#index with course_id" do
expect(get: "/api/v1/courses/1/chapters").to route_to(
controller: "api/v1/chapters",
action: "index",
course_id: "1"
)
end
end
describe "shallow routes" do
it "routes directly to chapter#show without course_id" do
expect(get: "/api/v1/chapters/1").to route_to(
controller: "api/v1/chapters",
action: "show",
id: "1"
)
end
end
end
Rails 提供了內建工具來檢視路由:
# 在 console 中檢視所有路由
Rails.application.routes.routes.map do |route|
{
method: route.verb,
path: route.path.spec.to_s,
controller: route.defaults[:controller],
action: route.defaults[:action]
}
end
# 使用 rake 任務
# rake routes
# 或
# rails routes
# 只看特定控制器的路由
# rails routes -c courses
# 搜尋特定路由
# rails routes -g enroll
最常見的誤解是認為每個資源都必須對應一個資料表。實際上,資源是業務概念的抽象:
# SessionsController 管理登入狀態 - 沒有對應的資料表
class Api::V1::SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = generate_jwt_token(user)
render json: { token: token }
else
render json: { error: 'Invalid credentials' }, status: :unauthorized
end
end
def destroy
# 使 token 失效的邏輯
head :no_content
end
end
# SearchesController 管理搜尋 - 可能跨多個資料表
class Api::V1::SearchesController < ApplicationController
def create
search = SearchService.new(search_params)
results = search.execute
# 可選:儲存搜尋歷史
SearchHistory.create(
user: current_user,
query: search_params,
results_count: results.count
)
render json: results
end
end
選擇正確的 HTTP 動詞不只是技術規範,更是溝通意圖:
# GET - 安全且冪等,不應有副作用
get '/courses/:id/preview' # 只讀取,不改變狀態
# POST - 不冪等,每次調用可能產生不同結果
post '/courses/:id/enroll' # 重複註冊可能失敗
# PUT/PATCH - 冪等,多次調用結果相同
patch '/courses/:id' # 多次更新同樣內容,結果一致
# DELETE - 冪等,刪除已刪除的資源應返回成功
delete '/courses/:id' # 資源不存在時仍返回 204
RESTful API 通過狀態碼傳達操作結果:
class Api::V1::CoursesController < ApplicationController
def create
course = Course.new(course_params)
if course.save
# 201 Created - 資源成功建立
render json: course, status: :created, location: api_v1_course_url(course)
else
# 422 Unprocessable Entity - 請求格式正確但無法處理
render json: { errors: course.errors }, status: :unprocessable_entity
end
end
def update
course = Course.find(params[:id])
if course.update(course_params)
# 200 OK - 成功且有回應內容
render json: course
else
render json: { errors: course.errors }, status: :unprocessable_entity
end
end
def destroy
course = Course.find(params[:id])
course.destroy
# 204 No Content - 成功但無回應內容
head :no_content
end
def enroll
course = Course.find(params[:id])
enrollment = current_user.enrollments.build(course: course)
if enrollment.save
render json: enrollment, status: :created
elsif enrollment.errors[:course].include?("already enrolled")
# 409 Conflict - 請求與資源當前狀態衝突
render json: { error: "Already enrolled" }, status: :conflict
else
render json: { errors: enrollment.errors }, status: :unprocessable_entity
end
end
end
練習目標:理解基本的 RESTful 路由設計,掌握巢狀資源和自定義動作的使用。
需求說明:
設計一個簡單的部落格系統,包含以下功能:
設計思考點:
解答:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# 文章資源 - 核心資源
resources :posts do
# 成員路由 - 作用於特定文章
member do
patch :publish # PATCH /api/v1/posts/:id/publish
patch :unpublish # PATCH /api/v1/posts/:id/unpublish
end
# 集合路由 - 作用於文章集合
collection do
get :published # GET /api/v1/posts/published - 獲取所有已發布文章
get :drafts # GET /api/v1/posts/drafts - 獲取所有草稿
get :search # GET /api/v1/posts/search?q=keyword
end
# 評論 - 使用 shallow routing 優化 URL
# 建立評論需要文章 context,但讀取/更新/刪除可以獨立進行
resources :comments, shallow: true do
member do
patch :approve # 批准評論(如果有審核機制)
patch :spam # 標記為垃圾評論
end
end
# 文章與標籤的關聯 - 透過獨立的關聯資源管理
resources :taggings, only: [:index, :create, :destroy]
# GET /api/v1/posts/:post_id/taggings - 查看文章的所有標籤
# POST /api/v1/posts/:post_id/taggings - 為文章添加標籤
# DELETE /api/v1/posts/:post_id/taggings/:id - 移除文章的標籤
end
# 標籤資源 - 獨立管理所有標籤
resources :tags, only: [:index, :create, :show, :update, :destroy] do
member do
get :posts # GET /api/v1/tags/:id/posts - 獲取該標籤的所有文章
end
collection do
get :popular # GET /api/v1/tags/popular - 熱門標籤
end
end
# 獨立的評論管理(可選,用於管理介面)
resources :comments, only: [:index] do
collection do
get :pending # 待審核的評論
get :recent # 最新評論
end
end
end
end
end
設計說明:
文章資源的設計理由:
publish
和 unpublish
作為 member 路由,因為它們作用於特定文章published
和 drafts
作為 collection 路由,返回特定狀態的文章集合評論的 shallow routing:
POST /posts/:post_id/comments
PATCH /comments/:id
/posts/:post_id/comments/:id
標籤關聯的處理:
taggings
作為關聯資源,明確表達「文章與標籤的關聯」這個概念常見錯誤:
POST /posts/:id/publish
,應該用 PATCH
,因為這是更新狀態/posts/:post_id/comments/:comment_id/replies/:reply_id
挑戰目標:設計複雜的工作流程路由,處理多角色互動和版本管理。
需求說明:
為 LMS 系統設計完整的作業提交和批改流程:
設計思考點:
解答:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :courses do
# 作業屬於特定課程
resources :assignments, shallow: true do
member do
# 講師操作
patch :publish # 發布作業(學生可見)
patch :close # 關閉提交(過了截止時間)
patch :extend # 延長截止時間
get :statistics # 查看提交統計
# 學生操作
get :requirements # 查看作業要求詳情
get :rubric # 查看評分標準
end
collection do
get :upcoming # 即將到期的作業
get :overdue # 已過期的作業
end
# 作業附件(參考資料、範例等)
resources :attachments, only: [:index, :create, :destroy], shallow: true
# 作業提交 - 核心工作流程
resources :submissions, shallow: true do
member do
# 狀態轉換(工作流程)
patch :submit # 學生提交(draft -> submitted)
patch :withdraw # 學生撤回(submitted -> draft)
patch :start_review # 開始批改(submitted -> reviewing)
patch :request_revision # 要求修改(reviewing -> revision_requested)
patch :resubmit # 重新提交(revision_requested -> submitted)
patch :grade # 完成批改並給分(reviewing -> graded)
patch :finalize # 最終確認(graded -> finalized)
# 查詢操作
get :feedback # 查看批改回饋
get :similarity # 查看相似度檢測結果(防抄襲)
end
collection do
get :my_submissions # 學生查看自己的所有提交
get :pending_review # 講師查看待批改的提交
get :reviewed # 講師查看已批改的提交
end
# 版本管理 - 每次提交可以有多個版本
resources :versions, only: [:index, :show, :create] do
member do
patch :restore # 恢復到特定版本
get :diff # 查看與前一版本的差異
end
end
# 批改評語和評分 - 作為獨立資源
resource :grade_record, only: [:show, :create, :update] do
member do
patch :approve # 審核通過評分
patch :dispute # 學生對評分提出異議
end
end
# 提交的附件(學生上傳的作業檔案)
resources :files, only: [:index, :create, :destroy], shallow: true
end
# 同儕評審設定
resource :peer_review_setting, only: [:show, :create, :update] do
member do
post :assign_reviewers # 分配評審者
get :assignments # 查看評審分配情況
end
end
end
end
# 獨立的同儕評審資源(跨作業管理)
resources :peer_reviews, only: [:index, :show, :create, :update] do
member do
patch :complete # 完成評審
patch :skip # 跳過(無法完成)
get :rubric # 查看評審標準
end
collection do
get :my_reviews # 我需要完成的評審
get :received # 我收到的評審
end
# 評審評論
resources :review_comments, only: [:create, :update, :destroy], shallow: true
end
# 作業模板(講師可以重複使用)
resources :assignment_templates, only: [:index, :show, :create, :update, :destroy] do
member do
post :duplicate # 複製模板
post :create_assignment # 從模板建立作業
end
end
end
end
end
設計說明:
核心設計理念:
工作流程的路由設計:
# 使用 PATCH 動詞表達狀態轉換
patch :submit # draft -> submitted
patch :start_review # submitted -> reviewing
patch :grade # reviewing -> graded
# 每個狀態轉換都是明確的業務動作
# 不使用通用的 update,而是具體的動作名稱
Shallow Routing 的應用:
# 建立時需要上下文
POST /courses/:course_id/assignments/:assignment_id/submissions
# 存取時可以獨立
GET /submissions/:id
PATCH /submissions/:id/grade
權限控制的考量:
# 在控制器中實現權限檢查
class SubmissionsController < ApplicationController
before_action :ensure_student, only: [:create, :submit, :withdraw]
before_action :ensure_instructor, only: [:grade, :request_revision]
before_action :ensure_owner_or_instructor, only: [:show]
end
常見的設計陷阱與解決方案:
陷阱 1:過度巢狀
# 錯誤:太深的巢狀
/courses/:id/assignments/:id/submissions/:id/versions/:id/files/:id
# 正確:使用 shallow routing
/files/:id # 文件有唯一 ID,不需要完整路徑
陷阱 2:動作思維
# 錯誤:RPC 風格
post '/submit_assignment'
post '/grade_submission'
# 正確:資源導向
patch '/submissions/:id/submit'
patch '/submissions/:id/grade'
陷阱 3:狀態混淆
# 錯誤:直接修改狀態
patch '/submissions/:id', { status: 'graded' }
# 正確:明確的業務動作
patch '/submissions/:id/grade', { score: 85, feedback: '...' }
擴展性考量:
這個設計展示了如何用 RESTful 方式處理複雜的業務工作流程,同時保持 URL 的清晰和可預測性。
Day 2 的專案結構:今天學習的路由設計直接影響了 config/routes.rb
的組織方式。良好的路由設計能讓專案結構更清晰。
Day 3 的 MVC 架構:路由是 MVC 的入口點,決定了請求如何流向控制器。RESTful 路由確保控制器的動作有明確的職責。
Day 4 的 ActiveRecord:資源導向的路由設計往往能指導我們更好地設計模型關係。如果路由很複雜,可能意味著模型設計需要重新考慮。
Day 6 的控制器設計:明天我們會深入探討控制器如何處理這些路由。RESTful 路由的七個標準動作會對應到控制器的七個方法。
Day 11 的 API 版本控制:今天初步接觸的命名空間會在版本控制中發揮關鍵作用。
Day 22-23 的 LMS 實戰:今天設計的路由結構將成為整個 LMS 系統的骨架。
今天我們探索了 Rails 的 RESTful 路由設計,這不只是學習一個框架的功能,更是理解一種設計哲學。
知識層面,我們學會了如何使用 resources
宣告路由,理解了巢狀路由、淺層路由、成員路由和集合路由的使用場景。我們也學會了如何用路由約束實現複雜的路由邏輯。
思維層面,我們理解了從動作思維到資源思維的轉變。這種轉變不是限制,而是一種更高層次的抽象,能幫助我們更好地組織業務邏輯。
實踐層面,我們能夠為複雜的業務系統設計清晰、一致的 API 結構。無論是簡單的 CRUD 操作,還是複雜的工作流程,都能用 RESTful 的方式優雅地表達。
完成今天的學習後,你應該能夠:
resources
快速建立標準的 CRUD 路由深入閱讀:
相關 Gem:
friendly_id
: 使用友善的 URL slug 替代數字 IDversionist
: 強大的 API 版本控制工具grape
: 另一種建構 RESTful API 的選擇明天我們將探討控制器與請求處理流程。如果說今天學習的路由是 API 的地圖,那明天就是學習如何在這張地圖上導航,處理每一個到達的請求。我們會深入理解請求從進入 Rails 到返回響應的完整生命週期,掌握 Strong Parameters 的安全機制,理解如何在控制器中優雅地處理各種情況。
準備好了嗎?讓我們繼續這段旅程,明天見!