
程序員都應該了解的7款API接口平臺
為了簡單起見,把所有內容都放在一個源中,當然也可以分成核心、端口和適配器。讓我們來看看這個示例的主要部分。
為了簡單起見,把所有內容都放在一個源代碼中,當然也可以分成 Core、Ports 和 Adapters 三部分。讓我們來看看這個示例的主要部分。
首先,我們需要定義要使用的云資源(又稱端口)。具體步驟如下:
bring ex;
bring cloud;
let tasks = new ex.Table(
name: "Tasks",
columns: {
"id" => ex.ColumnType.STRING,
"title" => ex.ColumnType.STRING
},
primaryKey: "id"
);
let counter = new cloud.Counter();
let api = new cloud.Api();
let path = "/tasks";
在這里,我們定義了一個 Winglang 表來保存 TODO 任務,該表只有兩列:任務 ID 和標題。為了保持簡單,我們使用 Winglang 計數器資源將任務 ID 實現為一個自動遞增的數字。最后,我們使用 Winglang API 資源公開 TODO 服務 API。
現在,我們要為四個 REST API 請求分別定義一個處理函數。獲取所有任務列表的方法如下:
api.get(
path,
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let rows = tasks.list();
let var result = MutArray<Json>[];
for row in rows {
result.push(row);
}
return cloud.ApiResponse{
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(result)
};
});
創建新任務記錄的方法如下:
api.post(
path,
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = "{counter.inc()}";
if let task = Json.tryParse(request.body) {
let record = Json{
id: id,
title: task.get("title").asStr()
};
tasks.insert(id, record);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(record)
};
} else {
return cloud.ApiResponse {
status: 400,
headers: {
"Content-Type" => "text/plain"
},
body: "Bad Request"
};
}
});
更新現有任務的方法如下:
api.put(
"{path}/:id",
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
if let task = Json.tryParse(request.body) {
let record = Json{
id: id,
title: task.get("title").asStr()
};
tasks.update(id, record);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(record)
};
} else {
return cloud.ApiResponse {
status: 400,
headers: {
"Content-Type" => "text/plain"
},
body: "Bad Request"
};
}
});
最后,刪除現有任務的方法如下:
api.delete(
"{path}/:id",
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
tasks.delete(id);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "text/plain"
},
body: ""
};
});
我們可以使用 Winglang 模擬器來試用這個 API:
我們可以編寫一個或多個測試來自動驗證應用程序接口:
bring http;
bring expect;
let url = "{api.url}{path}";
test "run simple crud scenario" {
let r1 = http.get(url);
expect.equal(r1.status, 200);
let r1_tasks = Json.parse(r1.body);
expect.nil(r1_tasks.tryGetAt(0));
let r2 = http.post(url, body: Json.stringify(Json{title: "First Task"}));
expect.equal(r2.status, 200);
let r2_task = Json.parse(r2.body);
expect.equal(r2_task.get("title").asStr(), "First Task");
let id = r2_task.get("id").asStr();
let r3 = http.put("{url}/{id}", body: Json.stringify(Json{title: "First Task Updated"}));
expect.equal(r3.status, 200);
let r3_task = Json.parse(r3.body);
expect.equal(r3_task.get("title").asStr(), "First Task Updated");
let r4 = http.delete("{url}/{id}");
expect.equal(r4.status, 200);
}
最后但并非最不重要的一點是,這項服務可以使用 Winglang CLI 部署到任何受支持的云平臺上。TODO 服務的代碼是完全云中立的,確保無需修改即可在不同平臺上兼容。
這個例子清楚地表明,Winglang 編程環境是快速開發此類服務的一流工具。如果這就是您所需要的一切,那么您就無需繼續閱讀了。接下來的內容就像一個白兔洞,在我們開始認真討論生產部署之前,需要解決多個非功能性問題。
請注意。即將發表的文章并非面向所有人,而是面向經驗豐富的云軟件架構師。
使用 REST API 快速構建 CRUD 服務與將其部署到生產環境之間存在巨大差距。生產環境需要考慮一系列非功能性問題。這些問題因業務環境而異。為小型團隊內部部署服務與為政府或金融機構部署相同功能有很大不同。
專家們對非功能性需求清單的理想結構爭論不休。作者更喜歡簡明扼要的高層次概述,并根據需要進行深入分析。以下是列出的四大非功能性需求,同時也承認這份清單并非詳盡無遺:
這些領域不是孤立的,而是存在重疊。一個緩慢或失靈的系統是無法使用的。相反,在國防部層面確保雜貨店庫存系統的安全可能會讓供應商滿意,但卻不會讓銀行家滿意。雖然分類并不完善,但它為進一步討論提供了一個框架。
上面介紹的 TODO 示例服務實現屬于所謂的無頭 REST API。這種方法側重于核心功能,將用戶體驗設計留給獨立的層。其實現方式通常是客戶端渲染(Client-Side Rendering)或服務器端渲染(Server Side Rendering),中間有一個前端后臺層(Backend for Frontend tier),或者使用多個作為 GraphQL 解析器(GraphQL Resolvers)運行的、范圍較窄的 REST API 服務。每種方法在特定情況下都有其優點。
主張支持 HTTP 內容協商(HTTP Content Negotiation),并為通過瀏覽器直接進行 API 交互提供最小用戶界面。雖然 Postman 或 Swagger 等工具可以促進 API 交互,但作為最終用戶體驗 API 可以提供寶貴的見解。這種基本的用戶界面,或者稱之為 “工程用戶界面”,通常就足夠了。
更簡單的解決方案是使用基本的 HTML 模板,并利用 HTMX 的功能和 CSS 框架(如 Bootstrap)進行增強。目前,Winglang 本身并不支持 HTML 模板,但對于基本用例,可以使用 TypeScript 輕松管理。例如,渲染單個任務行的實現方式如下:
import { TaskData } from "core/task";
export function formatTask(path: string, task: TaskData): string {
return `
<li class="list-group-item d-flex justify-content-between align-items-center">
<form hx-put="${path}/${task.taskID}" hx-headers='{"Accept": "text/plain"}' id="${task.taskID}-form">
<span class="task-text">${task.title}</span>
<input
type="text"
name="title"
class="form-control edit-input"
style="display: none;"
value="${task.title}">
</form>
<div class="btn-group">
<button class="btn btn-danger btn-sm delete-btn"
hx-delete="${path}/${task.taskID}"
hx-target="closest li"
hx-swap="outerHTML"
hx-headers='{"Accept": "text/plain"}'>?</button>
<button class="btn btn-primary btn-sm edit-btn">?</button>
</div>
</li>
`
}
這樣就會出現以下用戶界面畫面:
雖然不是超級華麗,但對于演示目的來說已經足夠好了。
即使是純粹的無頭 REST 應用程序接口(API),也需要考慮很強的可用性。API 調用應遵循 HTTP 方法、URL 格式和有效載荷的 REST 約定。正確記錄 HTTP 方法和潛在錯誤處理至關重要。需要記錄客戶端和服務器錯誤,將其轉換為適當的 HTTP 狀態代碼,并在響應正文中提供清晰的解釋信息。
由于需要使用 HTTP 請求中的 Content-Type 和 Accept 標頭,根據內容協商處理多個請求解析器和響應格式器,因此采用了以下設計方法:
遵循依賴反轉原則可確保系統核心與端口和適配器完全隔離。雖然可能有人傾向于將核心封裝在由 ResourceData 類型定義的通用 CRUD 框架中,但建議大家謹慎行事。這一建議源于幾個方面的考慮:
另一種選擇是放棄核心數據類型定義,完全依賴無類型的 JSON 接口,類似于 Lisp 編程風格。不過,考慮到 Winglang 的強類型性,決定不采用這種方法。
總的來說,TodoServiceHandler 非常簡單易懂:
bring "./data.w" as data;
bring "./parser.w" as parser;
bring "./formatter.w" as formatter;
pub class TodoHandler {
_path: str;
_parser: parser.TodoParser;
_tasks: data.ITaskDataRepository;
_formatter: formatter.ITodoFormatter;
new(
path: str,
tasks_: data.ITaskDataRepository,
parser: parser.TodoParser,
formatter: formatter.ITodoFormatter,
) {
this._path = path;
this._tasks = tasks_;
this._parser = parser;
this._formatter = formatter;
}
pub inflight getHomePage(user: Json, outFormat: str): str {
let userData = this._parser.parseUserData(user);
return this._formatter.formatHomePage(outFormat, this._path, userData);
}
pub inflight getAllTasks(user: Json, query: Map<str>, outFormat: str): str {
let userData = this._parser.parseUserData(user);
let tasks = this._tasks.getTasks(userData.userID);
return this._formatter.formatTasks(outFormat, this._path, tasks);
}
pub inflight createTask(
user: Json,
body: str,
inFormat: str,
outFormat: str
): str {
let taskData = this._parser.parsePartialTaskData(user, body);
this._tasks.addTask(taskData);
return this._formatter.formatTasks(outFormat, this._path, [taskData]);
}
pub inflight replaceTask(
user: Json,
id: str,
body: str,
inFormat: str,
outFormat: str
): str {
let taskData = this._parser.parseFullTaskData(user, id, body);
this._tasks.replaceTask(taskData);
return taskData.title;
}
pub inflight deleteTask(user: Json, id: str): str {
let userData = this._parser.parseUserData(user);
this._tasks.deleteTask(userData.userID, num.fromStr(id));
return "";
}
}
您可能會注意到,代碼結構與前面的設計圖略有不同。這些細微的調整在軟件設計中很正常;在整個過程中會出現新的見解,因此有必要進行調整。最顯著的區別是每個函數都定義了 user.Json 參數:Json 參數。我們將在下一節討論該參數的用途。
在沒有采取安全措施的情況下,將 TODO 服務暴露在互聯網上會帶來災難。黑客、無聊的青少年和專業攻擊者會迅速鎖定其公共 IP 地址。規則非常簡單:
反之,在服務中加入各種可以想象得到的安全措施會導致過高的運營成本。正如在以前的文章中所論述的,讓建筑師對其設計的成本負責可能會大大改變他們的設計方法:
我們需要的是對服務應用程序接口的合理保護,不能少也不能多。既然想嘗試全棧服務端渲染用戶界面,自然會選擇在一開始就強制用戶登錄,生成一個有合理有效期(比如一小時)的 JWT 令牌,然后用它對所有即將到來的 HTTP 請求進行身份驗證。
考慮到服務端渲染的具體情況,使用 HTTP Cookie 來傳遞會話令牌是一個自然的選擇(老實說是 ChatGPT 建議的)。對于客戶端渲染選項,可能需要使用通過 HTTP 請求頭授權字段傳遞的承載令牌。
有了包含用戶信息的會話令牌,就可以按用戶匯總 TODO 任務。雖然有許多方法可以將會話數據(包括用戶詳細信息)整合到域中,但選擇在本研究中重點關注 userID 和 fullName 屬性。
在用戶身份驗證方面,有多種選擇,尤其是在 AWS 生態系統內:
作為一名獨立的軟件技術研究人員,作者傾向于使用組件最少的最簡單解決方案,這樣也能滿足日常操作需求。由于現有的多賬戶/多用戶設置,利用 AWS 身份中心(詳見另一篇文章)是順理成章的一步。
集成后,AWS Identity Center 主屏幕如下所示:
這意味著,在作者的系統中,用戶、自己或客人可以使用相同的 AWS 憑據進行開發、管理以及示例或內務管理應用程序。
為了與 AWS Identity Center 集成,需要注冊應用程序,并提供一個實現所謂 “斷言消費者服務 URL(ACS URL)”的新端點。本文與 SAML 標準無關。只需指出,在 ChatGPT 和 Google 搜索的幫助下,就可以完成這項工作。在這里可以找到一些有用的信息。非常方便的是 TypeScript samlify 庫,它封裝了 SAML 登錄響應驗證過程的全部繁重工作。
作者最感興趣的是,這個可變點如何影響整個系統的設計。讓我們嘗試用半正式的數據流符號將其可視化:
這種表示法看似不尋常,但卻高保真地反映了數據是如何在系統中流動的。我們在這里看到的是著名的管道過濾器架構模式的一個特殊實例。
在這里,數據流經一個管道,每個過濾器執行一個定義明確的任務,實際上遵循了單一責任原則。這樣的安排允許更換過濾器,如果想改用簡單的 Basic HTTP 身份驗證、使用 HTTP 授權頭或使用不同的秘密管理策略來構建和驗證 JWT 令牌的話。
如果我們放大 Parse 和 Format 過濾器,就會看到分別使用 Content-Type 和 Accept HTTP 標頭的典型調度邏輯:
許多工程師將設計和架構模式與具體實現混為一談。這就忽略了模式的本質。
模式就是要找出一種合適的方法,以最少的干預來平衡相互沖突的力量。在構建基于云的軟件系統時,安全性至關重要,但成本或復雜性不應過高,因此這種理解至關重要。管道過濾器設計模式有助于有效解決此類設計難題。它允許對處理步驟進行模塊化和靈活配置,在本例中,這些步驟與身份驗證機制有關。
例如,雖然 SAML 身份驗證等強大的安全措施對于生產環境是必要的,但在自動端到端測試等場景中,它們可能會帶來不必要的復雜性和開銷。在這種情況下,基本 HTTP 身份驗證等更簡單的方法可能就足夠了,既能提供快速、經濟的解決方案,又不會損害系統的整體完整性。我們的目標是保持系統的核心功能和代碼庫的統一性,同時根據環境或特定要求改變身份驗證策略。
Winglang 獨特的預檢(Preflight)編譯功能允許在構建階段調整配置,從而消除了運行時的開銷。與其他中間件庫(如 Middy 和 AWS Power Tools for Lambda)相比,基于 Winglang 的解決方案的這一功能提供了管理身份驗證管道的更高效、更靈活的方法,因而具有顯著優勢。
因此,實施基本 HTTP 身份驗證只需修改身份驗證管道中的一個過濾器,系統的其余部分則保持不變:
由于一些技術上的限制,目前還無法在 Winglang 中直接實現 Pipe-and-Filters 功能,但可以通過結合 Decorator 和 Factory 設計模式來輕松模擬。具體如何實現,我們很快就會看到。現在,讓我們進入下一個主題。
在本刊物中,作者不會涉及生產運營的所有方面。這個主題很大,值得單獨出版。以下是作者認為最基本的內容:
要運行一項服務,我們需要知道它發生了什么,尤其是在出錯時。這可以通過結構化日志機制來實現。目前,Winglang 只提供了一個基本的 log(str) 函數。在調查中,需要更多的功能,并實現了一個可憐的結構化日志類:
// A poor man implementation of configurable Logger
// Similar to that of Python and TypeScript
bring cloud;
bring "./dateTime.w" as dateTime;
pub enum logging {
TRACE,
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
}
//This is just enough configuration
//A serious review including compliance
//with OpenTelemetry and privacy regulations
//Is required. The main insight:
//Serverless Cloud logging is substantially
//different
pub interface ILoggingStrategy {
inflight timestamp(): str;
inflight print(message: Json): void;
}
pub class DefaultLoggerStrategy impl ILoggingStrategy {
pub inflight timestamp(): str {
return dateTime.DateTime.toUtcString(std.Datetime.utcNow());
}
pub inflight print(message: Json): void {
log("{message}");
}
}
//TBD: probably should go into a separate module
bring expect;
bring ex;
pub class MockLoggerStrategy impl ILoggingStrategy {
_name: str;
_counter: cloud.Counter;
_messages: ex.Table;
new(name: str?) {
this._name = name ?? "MockLogger";
this._counter = new cloud.Counter();
this._messages = new ex.Table(
name: "{this._name}Messages",
columns: Map<ex.ColumnType>{
"id" => ex.ColumnType.STRING,
"message" => ex.ColumnType.STRING
},
primaryKey: "id"
);
}
pub inflight timestamp(): str {
return "{this._counter.inc(1, this._name)}";
}
pub inflight expect(messages: Array<Json>): void {
for message in messages {
this._messages.insert(
message.get("timestamp").asStr(),
Json{ message: "{message}"}
);
}
}
pub inflight print(message: Json): void {
let expected = this._messages.get(
message.get("timestamp").asStr()
).get("message").asStr();
expect.equal("{message}", expected);
}
}
pub class Logger {
_labels: Array<str>;
_levels: Array<logging>;
_level: num;
_service: str;
_strategy: ILoggingStrategy;
new (level: logging, service: str, strategy: ILoggingStrategy?) {
this._labels = [
"TRACE",
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"FATAL"
];
this._levels = Array<logging>[
logging.TRACE,
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.FATAL
];
this._level = this._levels.indexOf(level);
this._service = service;
this._strategy = strategy ?? new DefaultLoggerStrategy();
}
pub inflight log(level_: logging, func: str, message: Json): void {
let level = this._levels.indexOf(level_);
let label = this._labels.at(level);
if this._level <= level {
this._strategy.print(Json {
timestamp: this._strategy.timestamp(),
level: label,
service: this._service,
function: func,
message: message
});
}
}
pub inflight trace(func: str, message: Json): void {
this.log(logging.TRACE, func,message);
}
pub inflight debug(func: str, message: Json): void {
this.log(logging.DEBUG, func, message);
}
pub inflight info(func: str, message: Json): void {
this.log(logging.INFO, func, message);
}
pub inflight warning(func: str, message: Json): void {
this.log(logging.WARNING, func, message);
}
pub inflight error(func: str, message: Json): void {
this.log(logging.ERROR, func, message);
}
pub inflight fatal(func: str, message: Json): void {
this.log(logging.FATAL, func, message);
}
}
正如作者在評論中寫道的那樣,基于云的日志系統需要認真修改。不過,對于目前的調查來說,這已經足夠了。完全相信,日志記錄是任何服務規范不可分割的一部分,必須像測試核心功能一樣嚴格。為此,開發了一種簡單的機制來模擬日志,并根據預期進行檢查。
對于 REST API CRUD 服務,我們至少需要記錄三類日志:
此外,根據需要,可能需要將原始錯誤信息轉換為標準信息,例如,為了不給攻擊者提供可乘之機。
記錄多少細節取決于多種因素:部署目標、請求類型、特定用戶、錯誤類型、統計采樣等。在開發和測試模式下,我們通常會選擇記錄幾乎所有內容,并將原始錯誤信息直接返回到客戶端屏幕,以方便調試。在生產模式下,我們可能會選擇刪除一些敏感數據(因為有法規要求),返回 “Bad Request”(錯誤請求)等一般錯誤信息,而不提供任何詳細信息,并只對特定類型的請求進行統計抽樣記錄,以節省成本。
通過在每個請求處理管道中注入四個額外的過濾器,實現了靈活的日志配置:
這種結構雖然不是終極結構,但它提供了足夠的靈活性,可根據服務及其部署目標的具體情況,實施多種日志記錄和錯誤處理策略。
與日志一樣,Winglang 目前只提供了一個基本的 throw <str> 操作符,因此決定實現窮人版結構化異常:
// A poor man structured exceptions
pub inflight class Exception {
pub tag: str;
pub message: str?;
new(tag: str, message: str?) {
this.tag = tag;
this.message = message;
}
pub raise() {
let err = Json.stringify(this);
throw err;
}
pub static fromJson(err: str): Exception {
let je = Json.parse(err);
return new Exception(
je.get("tag").asStr(),
je.tryGet("message")?.tryAsStr()
);
}
pub toJson(): Json { //for logging
return Json{tag: this.tag, message: this.message};
}
}
// Standard exceptions, similar to those of Python
pub inflight class KeyError extends Exception {
new(message: str?) {
super("KeyError", message);
}
}
pub inflight class ValueError extends Exception {
new(message: str?) {
super("ValueError", message);
}
}
pub inflight class InternalError extends Exception {
new(message: str?) {
super("InternalError", message);
}
}
pub inflight class NotImplementedError extends Exception {
new(message: str?) {
super("NotImplementedError", message);
}
}
//Two more HTTP-specific, yet useful
pub inflight class AuthenticationError extends Exception {
//aka HTTP 401 Unauthorized
new(message: str?) {
super("AuthenticationError", message);
}
}
pub inflight class AuthorizationError extends Exception {
//aka HTTP 403 Forbidden
new(message: str?) {
super("AuthorizationError", message);
}
}
這些經驗凸顯了開發人員社區如何通過臨時變通辦法來彌補新語言的不足。盡管 Winglang 仍在不斷發展,但它的創新功能可以被利用來取得進步。
現在,是時候簡單了解一下清單上的最后一個生產主題了:
擴展是云計算開發的一個重要方面,但卻經常被誤解。有些人完全忽視了這一點,導致系統增長時出現問題。另一些人則過度工程化,希望從第一天起就成為一個 “FANG “系統。我們在 Kubernetes 上運行一切 “是技術圈子里的一句流行語,不管它是否適合手頭的項目。
忽視和過度設計這兩種極端情況都不理想。與安全性一樣,不應該忽視擴展性,但也不應該過分強調它。
在一定程度上,云平臺提供了具有成本效益的擴展機制。不同選項之間的選擇往往歸結為個人偏好或惰性,而非顯著的技術優勢。
謹慎的做法是從小規模、低成本開始,根據實際使用情況和性能數據而不是假設進行擴展。這種方法要求系統在設計上易于更改配置以適應擴展,Winglang本身并不支持這種方法,但通過進一步的開發和研究,這種方法肯定是可行的。舉例來說,讓我們考慮一下 AWS 生態系統內的擴展:
從本質上講,Winglang 的方法強調 “飛行前”(Preflight)和 “飛行中”(Inflight)階段,有望促進這些擴展策略,盡管它可能仍處于充分實現這一潛力的早期階段。對云軟件開發中可擴展性的探索強調從小處入手,根據實際數據做出決策,并保持靈活性以適應不斷變化的需求。
20 世紀 90 年代中期,作者從 Jim Coplien 那里學到了共性可變性分析法。從那時起,這種方法與 Edsger W. Dijkstra 的分層架構一起,成為作者軟件工程實踐的基石。共性變異性分析會問”在我們的系統中,哪些部分永遠不變,哪些部分可能需要改變?”開放-封閉原則 “規定,可變部分應可替換,而無需修改核心系統。
決定何時最終確定系統的穩定部分需要在靈活性和效率之間權衡利弊,從代碼生成到運行時的幾個階段都提供了固定的機會。動態語言的支持者可能會將這些決定推遲到運行時,以獲得最大的靈活性,而靜態編譯語言的支持者通常會盡早確保關鍵系統組件的安全。
Winglang 憑借其獨特的預檢編譯階段脫穎而出,允許在開發過程的早期修復云資源。在本文中,作者探討了 Winglang 如何通過靈活的過濾器管道解決云服務的非功能性問題,盡管這種粒度帶來了自身的復雜性。現在的挑戰是在不影響系統效率或靈活性的前提下管理這種復雜性。
雖然最終的解決方案還在制定過程中,但作者可以勾勒出一個能平衡這些力量的高層次設計方案:
這種設計結合了多種軟件設計模式,以達到理想的平衡。這一過程包括:
這種方法將復雜性轉向實現 Pipeline Builder機制和配置規范。經驗告訴我們可以實施這種機制(例如本文中描述的)。這通常需要一些通用編程和動態導入功能。提出一個好的配置數據模型更具挑戰性。
基于生成式人工智能的協同駕駛儀的最新進展提出了如何實現最具成本效益的結果的新問題。為了理解這個問題,讓我們重溫一下傳統的編譯和配置堆棧:
這種一般情況可能不適用于每個生態系統。以下是典型層的細分:
這種復雜的結構有其局限性。泛型會掩蓋核心語言,宏是不安全的,配置文件是偽裝得很差的腳本,代碼生成器依賴于不靈活的靜態模板。這些局限性正是作者認為當前內部開發平臺趨勢發展潛力有限的原因。
當我們期待生成式人工智能在簡化這些過程中發揮作用時,問題就來了:在軟件工程中,基于生成式人工智能的協同駕駛不僅能簡化流程,還能增強我們平衡共性與變異性的能力嗎?
原文鏈接:Implementing Production-grade CRUD REST API in Winglang