前後端不再打架,讓 ts-rest 發揮 TypeScript 的魔法吧!

閱讀時間約 11 分鐘
鱈魚的魚缸搬家了!新家文章皆有重新修訂,歡迎來新家看看喔。(´▽`ʃ♡ƪ)
raw-image

本文基於 TypeScript,如果沒有採用的小夥伴可以狠心離開惹。~(>_<。)


或者一起湊個熱鬧。(´▽`ʃ♡ƪ)


甚麼是 ts-rest?

ts-rest 是基於 TypeScript 的 RESTful API 工具,主要目的在於實現從介面定義開始,涵蓋至服務器、客戶端的全型別安全,而且無需額外、繁瑣的標註或生成過程。


風格類似 RPC,可以讓 API 呼叫更加直覺,配合 TSDoc 有更完整、詳盡的說明,可以大幅降低使用 API 的心智負擔。


以下範例以 Vue、NestJS 為例,相關套件版本如下:

  • @nestjs/core:10.3.8
  • @ts-rest/core:3.30.4
  • @ts-rest/nest:3.30.4
  • zod:3.23.8


從前有個後端和前端

他們想要合力串接一個資料,叫做 CollectionData。


通用資料

前後端手牽手,先討論想要的格式。(≧︶≦))( ̄▽ ̄ )ゞ


第一步定義傳輸層的 schema,方式與 zod 相同。

定義 Zod Object

import { z } from 'zod';

export const collectionDataSchema = z.object({
_id: z.string(),
/** 名稱 */
name: z.string(),
description: z.string(),
remark: z.string(),
});
export interface CollectionData extends z.infer<
typeof collectionDataSchema
> { }

// 用 type 也可以,取決於貴團隊的規範
// export type CollectionData = z.infer<typeof collectionDataSchema>


接著根據剛剛制定的 schema 和 API 功能定義合約:

ts-rest:Define Contract


import { AppRoute, ClientInferRequest, initContract } from '@ts-rest/core';
import { z } from 'zod';
import { collectionDataSchema } from './schema';

// 建立 collection-data
export const createCollectionDataDtoSchema = collectionDataSchema.omit({
_id: true,
}).partial({
remark: true,
description: true,
});

/**
* 使用 satisfies 是為了 AppRoute 的欄位提示又可以保留具體內容定義。
* 當然 as const 也行,只是輸入的過程不會有 AppRoute 欄位提示。
* const create: AppRoute 會遺失具體的內容定義,所以最後採用 satisfies。
*/
const create = {
method: 'POST',
path: '/v1/collection-data',
body: createCollectionDataDtoSchema,
responses: {
201: collectionDataSchema,
500: z.object({
message: z.string(),
}),
},
summary: '建立 collection-data',
} satisfies AppRoute

export const collectionDataContract = initContract().router({
create,
}, {
pathPrefix: '/api'
});

// 提前取出 client 視角的合約型別,方便前端使用
export interface CollectionDataContract extends ClientInferRequest<
typeof collectionDataContract
> { }


路人:「怎麼只有一個 create?ಠ_ಠ」

鱈魚:「我懶。(ツ)」

路人:(抽刀)

鱈魚:「刀下留魚啊 (#°Д°),是因為內容太多怕大家會累啦,用心良苦欸。」


NestJS 實作

依照官網步驟實作。

ts-rest:Nest


以下是一個簡單的範例:

...

@Controller()
export class CollectionDataController {
constructor(
private readonly loggerService: LoggerService,
private readonly collectionDataService: CollectionDataService,
) { }

@TsRestHandler(collectionDataContract.create, {
validateResponses: true
})
async create() {
return tsRestHandler(collectionDataContract.create, async ({
body: dto,
}) => {
const [error, data] = await to(this.collectionDataService.create(dto));
if (error) {
this.loggerService.error(`建立 CollectionData 錯誤 :`);
this.loggerService.error(error);

return {
status: 500,
body: {
message: '建立 CollectionData 發生錯誤,請稍後再試'
},
};
}

return {
status: 201,
body: data.toJSON(),
};
});
}
}


(那個奇怪的 await to 是因為我用了這個,不是寫錯喔。(。・∀・)ノ゙)


開啟 validateResponses 的話,ts-rest 會嚴格驗證 API 響應資料格式是否正確,錯誤的話會直接噴 500,這就表示後端最好在 e2e 測試時,就該把可能情境測出來,只要測試有過,資料格式就不可能錯。


前後端就不用再為了資料有坑吵架了。◝(≧∀≦)◟


Vue

前端用法有兩種,可以依照需求挑適合的用:


可以漸進式導入,兩個同時用也沒問題。

個人推薦 Vue Query,雖然有點門檻,可是功能很強很好用。(๑•̀ㅂ•́)و✧


如果都不喜歡,也可以自定義


驗證

前端如果呼叫 API 前想先驗證一次參數正確性。例如:驗證 collectionData API 的 body 資料是否正確。


只要取得 contract 中對應路徑的 zod schema 即可。

const createCollectionDataDto = collectionDataContract.create.body;

createCollectionDataDto.parse({...})


剩下就是 zod 的工作了。(o゚v゚)ノ

Zod:parse


型別

請求用的型別則是取用預先準備好的 CollectionDataContract,如下圖。

raw-image


回應型別則是可以使用 ServerInferResponseBody 推導:

type Data = ServerInferResponseBody<
typeof collectionDataContract.create, 201
>
raw-image

詳情可以看看 ts-rest 提供的 Inferring Types 工具可以推導更多型別內容。( •̀ ω •́ )y

ts-rest 還有很多內容,可以來官網逛逛。♪(^∇^*)

範例

以下是一個前端使用合約發出 API 的簡單範例:

import {
collectionDataContract,
type CollectionDataContract,
} from 'collection-data-contract';
import { initClient } from '@ts-rest/core';

const collectionApi = initClient(collectionDataContract);

/** 建立 Collection 資料 */
async function createData(
data: CollectionDataContract['create']['body'],
) {
const [error, result] = await to(
collectionApi.create({ body: data })
);

if (error || result.status !== 201) {
console.log('建立資料錯誤');
return;
}

console.log(`建立資料成功 : ${result.body.name}`);
}

(await to 的部分是因為我用了這個,不是因為寫錯喔。(。・∀・)ノ゙)


所以前後端要怎麼共享合約?

鱈魚:「真是個好問題,當然是複製貼...ᕕ( ゚ ∀。)ᕗ 」

路人:(拿起球棒)

鱈魚:「開玩笑的啦。∠( ᐛ 」∠)_」


目前我自己已知不錯的方式有:

  • 私有 npm
  • monorepo


個人比較推薦使用 monorepo,我們公司也是如此,至於怎麼用 Google 有很多教學,這裡就不贅述囉。ԅ( ˘ω˘ԅ)


以上恭喜你完全發揮 TypeScript 的魔法惹!ˋ( ° ▽、° )

從此前後端再也不用為資料不一致問題打架了。✧*。٩(ˊᗜˋ*)و✧


甚麼?你說還有很多事情可以吵?╭(°A ,°`)╮

那就超過本文的探討範圍惹,要打去練舞室打。ᕕ( ゚ ∀。)ᕗ

17會員
14內容數
各種鱈魚滾鍵盤的雜紀
留言0
查看全部
發表第一個留言支持創作者!
鱈魚的魚缸 的其他內容
分享一個有趣的套件,名為 await-to-js。 可以讓 Promise 與 await 的寫法更簡潔。
pipe 代表函數式程式設計中的概念,利用多個功能結合在一起,資料依序通過每個功能進行處理。文章中介紹了 pipe 的優點、兩個等效的程式碼比較以及 remeda 套件的使用。詳細介紹了使用 pipe 的好處,並提供了多個相關的例子,展示了 pipe 可讀性的提升。
分享一個有趣的套件,名為 await-to-js。 可以讓 Promise 與 await 的寫法更簡潔。
pipe 代表函數式程式設計中的概念,利用多個功能結合在一起,資料依序通過每個功能進行處理。文章中介紹了 pipe 的優點、兩個等效的程式碼比較以及 remeda 套件的使用。詳細介紹了使用 pipe 的好處,並提供了多個相關的例子,展示了 pipe 可讀性的提升。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
海士町是隱岐群島之一的中之島,位於日本海島根半島海岸約60公里處,這是一座只有一島一鎮的小島。 (面積:33.46平方公里,週長:89.1km) 受對馬暖流影響,擁有豐富的海洋和被選為名水百選之一的豐富泉水(天川水),島上自給自足,是一個半農半漁的社會。 本地區自古以來就被認為是食品生產國,是海
Thumbnail
你知道緣份這種事有多麼奇妙嗎? 前陣子在誠品的新書陳列架上看到一本名為「城北舊事」的散文書籍,第一眼看到上面的推薦序寫著「北投最溫柔的地誌書寫」就吸引到了我,從小到大都一直在這個地方生活著,因此看到家鄉相關的書籍便頓時來了興趣。 開始深入閱讀後才發現這是一本很有趣的作品,雖為散文卻擁有小
Thumbnail
最近是轉職潮,有大量的新朋友入職,所以特別想寫一篇來提醒大家,入職該注意的法律知識,就是簽訂勞動契約前後該注意的事情。 往往關鍵的權利義務就在契約裡,而我們簽約時,更要
Thumbnail
當我剛開始接觸這個領域的時候,經常會看到有人在討論: 到底要做前端還是後端工程師呢?後端工程師賺的比前端工程師多?前端工程師轉後端工程師?那前端與後端到底是什麼呢? 這篇文章提供了前端與後端的基本概念並舉例來說明。同時也介紹了前端的三大要素以及後端的運作原理,對於想深入瞭解前後端的讀者會非常有幫助。
Thumbnail
談到面試這件事,我可能真的有很多甘苦談可以分享給大家。發表《面試不成的12個原因(上)最好是不符所需啦!》及《面試不成的12個原因(下)被當分母了》兩篇文章後,受到不少的關注,我決定再來分享跟面試有關的三個彩蛋。 面試是每個上班族都會遇到的。本文這三個彩蛋,或許你可以稱之為經典、離譜、不專業⋯⋯
Thumbnail
保單內容一條款為主,但大部分的保險從業人員都沒看過條款,甚至不知道理賠限制等等細節,所以可以找專業的保險經紀人,來說明各家的條款差異!畢竟理賠與否不是業務員說的算,也不是保險公司說的算,而是條款!
Thumbnail
講完 get 來講 post,那這兩個 call api 的方法有什麼差別呢?先記得一個原則,要傳遞機密的資料用 post,如果要傳遞的參數被別人知道也無所謂就用 get post 是傳遞資料到後台,資料不會顯示在 url 上,get 是傳遞參數到後台,參數會直接顯示在 url 上和後端的終端上,像
Thumbnail
前端和後端溝通,最常用就是 get 和 post 後端 先在 django 專案這邊的 views.py 內建立一個讓前端以 get 方法呼叫的 api function1 內的第一個參數是前端的 request,之後第二個參數第三個參數可以隨便你設,這倒是沒有限制 用 request.GET 這個
Thumbnail
本文使用網站的 FB 登入做示範 採用 Laravel 8 + Socialite 5 使用 Session 記錄狀態 不同版本可能會有些許語法及方法上的差異,請自行調整 前言 最近因為碰到需要實作 OAuth 第三方登入的需求,只好把之前隨便看看的東西撿回來研究並實作。不過我找到多數現存的中文文章
Thumbnail
文、圖/橘子關懷基金會提供   橘子關懷基金會十週年展開「前進南極點」計劃,為支援台灣史上第一支長征南極點的探險隊伍,橘子集團展現堅強的軟硬體實力,以高科技打造「TAIPEI前進南極點任務控制中心」,再結合內部全新開發的「teamup!」管理APP,協助後勤團隊精準掌握遠在12,790公里的
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
海士町是隱岐群島之一的中之島,位於日本海島根半島海岸約60公里處,這是一座只有一島一鎮的小島。 (面積:33.46平方公里,週長:89.1km) 受對馬暖流影響,擁有豐富的海洋和被選為名水百選之一的豐富泉水(天川水),島上自給自足,是一個半農半漁的社會。 本地區自古以來就被認為是食品生產國,是海
Thumbnail
你知道緣份這種事有多麼奇妙嗎? 前陣子在誠品的新書陳列架上看到一本名為「城北舊事」的散文書籍,第一眼看到上面的推薦序寫著「北投最溫柔的地誌書寫」就吸引到了我,從小到大都一直在這個地方生活著,因此看到家鄉相關的書籍便頓時來了興趣。 開始深入閱讀後才發現這是一本很有趣的作品,雖為散文卻擁有小
Thumbnail
最近是轉職潮,有大量的新朋友入職,所以特別想寫一篇來提醒大家,入職該注意的法律知識,就是簽訂勞動契約前後該注意的事情。 往往關鍵的權利義務就在契約裡,而我們簽約時,更要
Thumbnail
當我剛開始接觸這個領域的時候,經常會看到有人在討論: 到底要做前端還是後端工程師呢?後端工程師賺的比前端工程師多?前端工程師轉後端工程師?那前端與後端到底是什麼呢? 這篇文章提供了前端與後端的基本概念並舉例來說明。同時也介紹了前端的三大要素以及後端的運作原理,對於想深入瞭解前後端的讀者會非常有幫助。
Thumbnail
談到面試這件事,我可能真的有很多甘苦談可以分享給大家。發表《面試不成的12個原因(上)最好是不符所需啦!》及《面試不成的12個原因(下)被當分母了》兩篇文章後,受到不少的關注,我決定再來分享跟面試有關的三個彩蛋。 面試是每個上班族都會遇到的。本文這三個彩蛋,或許你可以稱之為經典、離譜、不專業⋯⋯
Thumbnail
保單內容一條款為主,但大部分的保險從業人員都沒看過條款,甚至不知道理賠限制等等細節,所以可以找專業的保險經紀人,來說明各家的條款差異!畢竟理賠與否不是業務員說的算,也不是保險公司說的算,而是條款!
Thumbnail
講完 get 來講 post,那這兩個 call api 的方法有什麼差別呢?先記得一個原則,要傳遞機密的資料用 post,如果要傳遞的參數被別人知道也無所謂就用 get post 是傳遞資料到後台,資料不會顯示在 url 上,get 是傳遞參數到後台,參數會直接顯示在 url 上和後端的終端上,像
Thumbnail
前端和後端溝通,最常用就是 get 和 post 後端 先在 django 專案這邊的 views.py 內建立一個讓前端以 get 方法呼叫的 api function1 內的第一個參數是前端的 request,之後第二個參數第三個參數可以隨便你設,這倒是沒有限制 用 request.GET 這個
Thumbnail
本文使用網站的 FB 登入做示範 採用 Laravel 8 + Socialite 5 使用 Session 記錄狀態 不同版本可能會有些許語法及方法上的差異,請自行調整 前言 最近因為碰到需要實作 OAuth 第三方登入的需求,只好把之前隨便看看的東西撿回來研究並實作。不過我找到多數現存的中文文章
Thumbnail
文、圖/橘子關懷基金會提供   橘子關懷基金會十週年展開「前進南極點」計劃,為支援台灣史上第一支長征南極點的探險隊伍,橘子集團展現堅強的軟硬體實力,以高科技打造「TAIPEI前進南極點任務控制中心」,再結合內部全新開發的「teamup!」管理APP,協助後勤團隊精準掌握遠在12,790公里的