iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
佛心分享-IT 人職涯歷練

從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始系列 第 10

Day 10|一支 API 到底怎麼從前端一路跑到後端?

  • 分享至 

  • xImage
  •  

上一篇我們追了一次:

run()
↓
useRequest
↓
Service.getById()
↓
Response
↓
State / Form

但如果今天 Bug 不在前端呢?

例如:

前端明明有送 Request
↓
Network 也有 Response
↓
但資料就是不對

這時候就不能只停在:

InvoiceService.getById(id)

而要繼續往後追。

也就是:

這支 API 到後端之後,到底經過哪些地方?


先建立一張最基本的地圖

我現在會先把前後端流程想成:

Frontend Component
↓
Service
↓
HTTP Request
↓
Backend Controller
↓
Service
↓
Repository
↓
Database
↓
Response
↓
Frontend

這張圖非常重要。

因為每次遇到資料問題,都可以問:

問題可能在哪一層?

而不是看到錯誤就整個專案亂翻。


前端:Component 不一定直接寫 API

假設 Component 裡看到:

const result = await InvoiceService.getById(id);

這裡的:

InvoiceService.getById(id)

通常不是資料庫查詢。

它只是前端的一層封裝。

Service 裡可能長得像:

export const InvoiceService = {
  getById(id: number) {
    return request(`/invoices/${id}`);
  },
};

這時候資料流就是:

Component
↓
InvoiceService.getById(id)
↓
GET /invoices/123

所以如果想知道:

「前端到底打哪支 API?」

就可以繼續進 Service 找真正的:

HTTP Method
+
URL

HTTP Method 先告訴我「想做什麼」

常見的 Method 可以先這樣理解:

GET
→ 取得資料

POST
→ 新增資料

PATCH
→ 修改部分資料

PUT
→ 更新資源

DELETE
→ 刪除資料

例如:

GET /invoices/123

先不用看後端程式,就可以大概知道:

要取得 id = 123 的 Invoice。

如果是:

PATCH /invoices/123

則是在告訴後端:

我要修改這筆 Invoice 的某些內容。

所以開始追 API 時,我會先記:

Method 是什麼?
URL 是什麼?
Request 帶了什麼?

Request 到後端後,先找 Controller

假設後端是 Spring / Kotlin 類型的專案,可能會看到:

@RestController
@RequestMapping("/invoices")
class InvoiceController(
    private val invoiceService: InvoiceService
) {
    @GetMapping("/{id}")
    fun getById(
        @PathVariable id: Long
    ): InvoiceViewModel {
        return invoiceService.getById(id)
    }
}

第一次看到這些 Annotation 很容易先卡住:

@RestController 是什麼?
@RequestMapping 是什麼?
@GetMapping 又是什麼?
@PathVariable 又是什麼?

但我現在不會先全部查。

我會先找:

URL 對不對得起來?

前端:

GET /invoices/123

後端:

@RequestMapping("/invoices")

搭配:

@GetMapping("/{id}")

合起來就是:

/invoices/{id}

所以:

/invoices/123

會進到這個 function。


Controller 的工作可以先想成「入口」

所以:

fun getById(
    @PathVariable id: Long
): InvoiceViewModel {
    return invoiceService.getById(id)
}

可以先翻成人話:

收到 GET /invoices/123
↓
把 URL 裡的 123 取成 id
↓
交給 invoiceService.getById(id)
↓
把結果回傳

這時候 Controller 本身通常不一定負責真正查資料。

它比較像:

後端 API 的入口。


接著進 Service

Controller 呼叫:

invoiceService.getById(id)

所以往下追 Service:

@Service
class InvoiceService(
    private val invoiceRepository: InvoiceRepository
) {
    fun getById(id: Long): InvoiceViewModel {
        val invoice =
            invoiceRepository.findById(id)
                .orElseThrow()

        return InvoiceViewModel(
            id = invoice.id,
            number = invoice.number,
            discount = invoice.discount
        )
    }
}

這時候可以先不管每個 Kotlin 語法。

只看流程:

Service 收到 id
↓
Repository 查資料
↓
拿到 invoice
↓
整理成 InvoiceViewModel
↓
回傳

所以 Service 可以先理解成:

放商業邏輯、流程處理與資料整理的地方。


Repository 才開始靠近 Database

剛才看到:

invoiceRepository.findById(id)

Repository 的角色可以先想成:

負責跟資料存取層溝通。

例如:

interface InvoiceRepository :
    JpaRepository<Invoice, Long>

如果使用 JPA / Hibernate,很多基本查詢不一定需要自己寫 SQL。

像:

findById(id)

框架可以幫忙轉成對資料庫的查詢。

所以資料流又多一層:

Service
↓
Repository
↓
Database

那 Entity 又是什麼?

假設看到:

@Entity
class Invoice(
    @Id
    val id: Long,

    val number: String,

    val discount: BigDecimal?
)

可以先把 Entity 理解成:

後端程式用來對應資料庫資料結構的物件。

例如 Database:

invoice

id
number
discount

在程式裡可能就是:

Invoice

所以:

Database Row
↓
ORM / JPA
↓
Entity

這裡又接回 Service。


為什麼還要 ViewModel?不能直接回 Entity 嗎?

剛才 Service 最後沒有直接:

return invoice

而是:

return InvoiceViewModel(
    id = invoice.id,
    number = invoice.number,
    discount = invoice.discount
)

這是因為 API Response 不一定需要把整個 Entity 全部回給前端。

例如 Entity 可能還有:

internalNote
createdBy
updatedAt
relations
其他內部欄位

但前端 Detail 頁可能只需要:

id
number
discount

所以可以另外整理成:

data class InvoiceViewModel(
    val id: Long,
    val number: String,
    val discount: BigDecimal?
)

資料流:

Database
↓
Entity
↓
Service 整理
↓
ViewModel
↓
Response
↓
Frontend

這也是為什麼:

Database 有某個欄位,不代表 API Response 一定會有。

中間還可能經過 ViewModel / DTO 的轉換。


把整條 GET API 接起來

現在就可以完整畫:

Frontend Component
↓
InvoiceService.getById(123)
↓
GET /invoices/123
↓
Controller
↓
invoiceService.getById(123)
↓
Repository.findById(123)
↓
Database
↓
Invoice Entity
↓
轉成 InvoiceViewModel
↓
HTTP Response
↓
Frontend result
↓
State / Form
↓
Render

這就是一支 API 從前端跑到後端,再跑回來的基本流程。


那 Debug 時到底要怎麼用?

假設前端發現:

discount 顯示錯誤

以前可能直接改:

form.setFieldsValue(...)

但現在可以先一路確認:

① UI 顯示錯?
↓
② API Response 裡 discount 是什麼?
↓
③ Response 已經錯了嗎?
↓
④ ViewModel 怎麼組 discount?
↓
⑤ Entity 裡 discount 是什麼?
↓
⑥ Database 真正存的是什麼?

如果 Network Response 已經是:

{
  "discount": 10
}

但 UI 顯示:

20

問題比較可能在前端。

反過來,如果 Response 就已經是:

{
  "discount": 20
}

那就不用一直改前端 Input。

應該繼續往:

Controller
Service
Repository
Database

追。


PATCH 也是同一條路,只是方向不同

假設:

使用者修改 discount
↓
PATCH /invoices/123

後端可能變成:

Controller
↓
收到 Request Body
↓
Service
↓
找到原本 Entity
↓
修改欄位
↓
Repository save
↓
Database
↓
Response

所以之前提到的:

「PATCH 200,但重新 GET 又變回舊值」

就可以開始拆:

Frontend 有送新值嗎?
↓
Controller 有收到嗎?
↓
Service 有套用嗎?
↓
Repository 有 save 嗎?
↓
Database 有真的改嗎?
↓
GET 時 ViewModel 又回了什麼?

這時候 Debug 就不再只是猜。

而是沿著資料流逐層排除。


我現在讀後端,不會要求自己一次全部懂

看到 Kotlin 專案時,我以前很容易卡在:

annotation
generic
nullable
JPA
Hibernate
repository
data class

然後覺得:

我後端完全看不懂。

但其實第一輪只要先找:

Controller
↓
Service
↓
Repository
↓
Entity / Database
↓
ViewModel
↓
Response

先知道資料去哪裡。

陌生語法再慢慢補。

這和前面讀 React 的方法其實完全一樣:

先讀資料流,再補語法。


今天先記住這張圖

Frontend
│
├─ Component
│
├─ Request Hook
│
└─ Service
     ↓
HTTP Request
     ↓
Backend
│
├─ Controller
│
├─ Service
│
├─ Repository
│
└─ Entity
     ↓
Database

Database
↓
Entity
↓
ViewModel / DTO
↓
Response
↓
Frontend

所以以後看到:

SomeService.getById(id)

不要只把它當成:

「取得資料的 function。」

它背後其實可能連著整條:

前端
↓
HTTP
↓
後端
↓
資料庫
↓
再一路回到畫面

而開始看懂這張地圖之後,我也更容易知道:

這個 Bug 到底該從哪一層開始查。

下一篇可以接著處理一個我以前非常容易混亂的問題:

nullundefined、空字串、0 到底差在哪?為什麼前端常常要一直防空值?


上一篇
Day 09|useRequest 到底在幹嘛?先追 API 從哪裡出去、資料又跑去哪裡
下一篇
Day 11|null、undefined、空字串、0 到底差在哪?前端為什麼一直在防空值?
系列文
從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言