iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Modern Web

30 天 Rails 新手村:從工作專案學會 Ruby on Rails系列 第 6

Day 5: RESTful 路由設計 - 用資源思維重新理解 Web API

  • 分享至 

  • xImage
  •  

如果你來自 Express 的世界,你可能習慣了自由定義路由的方式。想要一個登入端點?就寫 POST /login。需要取得用戶資料?那就 GET /getUserInfo。每個路由都是根據功能命名,直觀且靈活。在 Spring Boot 中,你可能用 @GetMapping("/api/users/search") 來處理搜尋,用 @PostMapping("/api/users/activate") 來啟用帳號。FastAPI 讓你用 Python 的函式裝飾器定義任何你想要的路徑結構。

今天我們要探討的是 Rails 如何用完全不同的思維來設計路由系統。Rails 不是限制你的創意,而是提供了一種經過二十年驗證的模式,讓你的 API 更一致、更可預測、更容易維護。當你真正理解 REST 的資源思維後,你會發現那些看似任意的路由命名,其實都在試圖表達同一個概念:對資源的操作。

這個知識點在我們最終的 LMS 系統中無處不在。課程、章節、課時、作業、討論,這些都是資源。學生註冊課程、提交作業、發表評論,這些都是對資源的操作。RESTful 路由不只是技術規範,更是一種組織業務邏輯的思維框架。

REST 的本質:資源與表現

從動作思維到資源思維

讓我們先看看不同框架處理「使用者管理」的典型方式:

// 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 堅持 RESTful

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 設計

設計 LMS 的核心路由結構

讓我們為 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

深入理解:RESTful 設計的哲學

資源不等於資料表

最常見的誤解是認為每個資源都必須對應一個資料表。實際上,資源是業務概念的抽象:

# 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 動詞的語意

選擇正確的 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

實踐練習:設計你的 API

基礎練習:部落格系統路由設計(預計 30 分鐘)

練習目標:理解基本的 RESTful 路由設計,掌握巢狀資源和自定義動作的使用。

需求說明
設計一個簡單的部落格系統,包含以下功能:

  1. 文章的完整 CRUD 操作(建立、讀取、更新、刪除)
  2. 文章可以發布(publish)和下架(unpublish)
  3. 評論系統,評論屬於特定文章
  4. 標籤系統,文章和標籤是多對多關係
  5. 搜尋功能,可以搜尋文章標題和內容

設計思考點

  • 發布和下架是改變文章狀態,應該用 PATCH 動詞和 member 路由
  • 評論依附於文章存在,適合用巢狀資源,但可以考慮 shallow routing
  • 標籤的關聯可以透過獨立的關聯資源處理

解答

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

設計說明

  1. 文章資源的設計理由

    • 使用標準的 RESTful 七個動作處理基本 CRUD
    • publishunpublish 作為 member 路由,因為它們作用於特定文章
    • publisheddrafts 作為 collection 路由,返回特定狀態的文章集合
  2. 評論的 shallow routing

    • 建立評論時需要知道屬於哪篇文章:POST /posts/:post_id/comments
    • 但更新或刪除評論時,只需要評論 ID:PATCH /comments/:id
    • 這避免了冗長的 URL 如 /posts/:post_id/comments/:id
  3. 標籤關聯的處理

    • 使用 taggings 作為關聯資源,明確表達「文章與標籤的關聯」這個概念
    • 標籤本身作為獨立資源管理,可以獨立於文章進行 CRUD 操作
  4. 常見錯誤

    • 不要用 POST /posts/:id/publish,應該用 PATCH,因為這是更新狀態
    • 不要過度巢狀,如 /posts/:post_id/comments/:comment_id/replies/:reply_id
    • 搜尋功能用 GET 而非 POST,因為搜尋是安全且冪等的操作

進階挑戰:LMS 作業系統工作流程(預計 1 小時)

挑戰目標:設計複雜的工作流程路由,處理多角色互動和版本管理。

需求說明
為 LMS 系統設計完整的作業提交和批改流程:

  1. 講師可以建立作業,設定截止日期和要求
  2. 學生可以查看作業要求、提交作業、查看批改結果
  3. 支援多次提交(每次提交是一個新版本)
  4. 講師可以批改作業、給分、寫評語、退回重做
  5. 支援同儕評審:學生可以互相評審作業
  6. 作業可以有附件(如參考資料、範例檔案)

設計思考點

  • 提交應該是獨立的資源,而不只是作業的屬性
  • 版本管理需要能追蹤每次提交
  • 同儕評審需要權限控制
  • 批改和評分是對提交的操作,不是對作業的操作

解答

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

設計說明

  1. 核心設計理念

    • 提交(Submission)作為一等公民:不是作業的屬性,而是獨立的資源,因為提交有自己的生命週期和狀態
    • 版本(Version)管理:每次修改都創建新版本,保留完整歷史
    • 評分記錄(GradeRecord)獨立管理:便於審計和爭議處理
  2. 工作流程的路由設計

    # 使用 PATCH 動詞表達狀態轉換
    patch :submit       # draft -> submitted
    patch :start_review # submitted -> reviewing
    patch :grade        # reviewing -> graded
    
    # 每個狀態轉換都是明確的業務動作
    # 不使用通用的 update,而是具體的動作名稱
    
  3. Shallow Routing 的應用

    # 建立時需要上下文
    POST /courses/:course_id/assignments/:assignment_id/submissions
    
    # 存取時可以獨立
    GET /submissions/:id
    PATCH /submissions/:id/grade
    
  4. 權限控制的考量

    # 在控制器中實現權限檢查
    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
    
  5. 常見的設計陷阱與解決方案

    陷阱 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: '...' }
    
  6. 擴展性考量

    • 模板系統允許講師重用作業設定
    • 同儕評審作為獨立模組,可以靈活配置
    • 版本系統支援無限次提交和歷史追蹤

這個設計展示了如何用 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 系統的骨架。

總結:用資源思維重塑 API 設計

今天我們探索了 Rails 的 RESTful 路由設計,這不只是學習一個框架的功能,更是理解一種設計哲學。

知識層面,我們學會了如何使用 resources 宣告路由,理解了巢狀路由、淺層路由、成員路由和集合路由的使用場景。我們也學會了如何用路由約束實現複雜的路由邏輯。

思維層面,我們理解了從動作思維到資源思維的轉變。這種轉變不是限制,而是一種更高層次的抽象,能幫助我們更好地組織業務邏輯。

實踐層面,我們能夠為複雜的業務系統設計清晰、一致的 API 結構。無論是簡單的 CRUD 操作,還是複雜的工作流程,都能用 RESTful 的方式優雅地表達。

自我檢核清單

完成今天的學習後,你應該能夠:

  • [ ] 解釋 RESTful 設計與傳統 RPC 風格 API 的差異
  • [ ] 使用 resources 快速建立標準的 CRUD 路由
  • [ ] 正確選擇使用巢狀路由還是淺層路由
  • [ ] 區分什麼時候使用 member 路由,什麼時候使用 collection 路由
  • [ ] 將複雜的業務邏輯轉換為資源導向的設計
  • [ ] 在 LMS 專案中設計清晰的路由結構

延伸資源

深入閱讀:

相關 Gem:

  • friendly_id: 使用友善的 URL slug 替代數字 ID
  • versionist: 強大的 API 版本控制工具
  • grape: 另一種建構 RESTful API 的選擇

明天我們將探討控制器與請求處理流程。如果說今天學習的路由是 API 的地圖,那明天就是學習如何在這張地圖上導航,處理每一個到達的請求。我們會深入理解請求從進入 Rails 到返回響應的完整生命週期,掌握 Strong Parameters 的安全機制,理解如何在控制器中優雅地處理各種情況。

準備好了嗎?讓我們繼續這段旅程,明天見!


上一篇
Day 4: ActiveRecord 基礎與資料建模 - 理解 Rails 的資料哲學
系列文
30 天 Rails 新手村:從工作專案學會 Ruby on Rails6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言