# 使用AWS輕鬆打造RESTful API吧! ## 前言 對於一個沒有接觸過網頁後端,但想要接觸或瞭解後端的人,搜尋網頁後端的資料可能會出現從語言選擇、框架選擇、環境建置、資料庫選擇、伺服器作業系統到部署等非常多的資訊,這對於一個新手來說是非常容易卻步的。 所以寫這篇主要的目的,是希望提供給初步想接觸網頁後端,卻不知道從哪下手的人一個簡單快速產生一份屬於自己 API 的方式,來初步體驗後端,而上一段提到的基礎們日後還是需要慢慢去了解。 今天會使用到 AWS 提供的 API Gateway、Lambda、DynamoDB,來完成簡單的 TodoList API。 由於本篇主軸是透過 AWS 提供的 Service 來快速產生RESTful API,關於每一個 Service 詳細的介紹日後會再各自出介紹文章。 ## 1. 建立 DynamoDB 資料表 首先我們進到 DynamoDB 的服務中, create table,並且在 Table name 中輸入 `todo`,並在[Partition key](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey)中輸入 `id`,接著點選 ```create table``` 完成資料表建立。 ![](https://i.imgur.com/XgPBvZB.png) 建立完成後,就會發現 **Tables** 中有剛剛建立的資料表,我們進入資料表中並點選 ```Explore table Items```,可以看到資料表目前是空的,下一步我們就要透過 Lambda 來操作資料表。 ![](https://i.imgur.com/CJfcy4p.png) ## 2. 建立 Lambda function 上一節中,我們已經完成初步的 DB 建置了,接著我們進到 Lambda 服務中,並且點選 ```create function```。 接著我們透過以下步驟做初始化設定: 1. **選擇模板**:我們可以看到 Lambda 提供從頭開始寫也有簡易的模板,在這邊我們選從頭開始撰寫 2. **輸入 function 名稱**:在 Function name 輸入 `createTodo` 3. **選擇語言**:Lambda有提供多種語言可以選擇,可以根據自己喜好或需求做選擇,本篇將使用Node18.x 來做示範 ![](https://i.imgur.com/yBfnBtK.png) 4. **為 function 建立 role**:因為我們要操作 DynamoDB,前面我們也沒建立過 role,所以這邊我們要建立一個擁有操作 DynameDB 權限的 role : 1. 點開 **Change default execution role** 2. 選擇 **Create a new role from AWS policy templates** 3. Role name中輸入`DynamoDBUser` 4. Policy templates 中選擇 **Simple microservice permissions**及**Test harness permissions** 5. **點選 create function** 接著會進到 function 裡面,並看到基礎的程式碼,代表 lambda function 成功建立了,我們重複步驟把`listTodos`、`updateTodo`、`removeTodo`也建立起來。 ![](https://i.imgur.com/Q85qUVb.png) ## 3. 撰寫並發佈 Lambda function 上一章節我們已經建立好我們需要使用到的 function 了,這章節我們將撰寫並發布我的的API function,大家可以試著自己寫看看,如果只是想體驗建立到發布的人可以直接複製程式碼。 ### createTodo 本篇為簡化教學,使用建立時的 timestamp 作為 id,正式開發可使用 [uuid](https://www.npmjs.com/package/uuid) 等套件建立 id 。 ```javascript= import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({}); const dynamo = DynamoDBDocumentClient.from(client); const tableName = "todo"; export const handler = async (event, context) => { let body; let statusCode = 200; const headers = { "Content-Type": "application/json", }; try { const { name } = JSON.parse(event.body) // verify the request body. if (!name) throw new Error(`Invalid input: missing key 'name'`) // createTodo. const param = { id: `${new Date().getTime()}`, name, completed: false, __typename: `todo`, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } await dynamo.send(new PutCommand({ TableName: tableName, Item: param, })); const result = { success: true, message: `OK`, value: param } body = result } catch (err) { statusCode = 400; body = err.message ?? err; } finally { body = JSON.stringify(body); } return { statusCode, body, headers, }; }; ``` ### listTodos ```javascript= import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, ScanCommand, } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({}); const dynamo = DynamoDBDocumentClient.from(client); const tableName = "todo"; export const handler = async (event, context) => { let body; let statusCode = 200; const headers = { "Content-Type": "application/json", }; try { // getTodos. const { Items } = await dynamo.send( new ScanCommand({ TableName: tableName }) ); const result = { success: true, message: `OK`, value: Items } body = result } catch (err) { statusCode = 400; body = err.message ?? err; } finally { body = JSON.stringify(body); } return { statusCode, body, headers, }; }; ``` ### updateTodo ```javascript= import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({}); const dynamo = DynamoDBDocumentClient.from(client); const tableName = "todo"; export const handler = async (event, context) => { let body; let statusCode = 200; const headers = { "Content-Type": "application/json", }; try { const { id, name, completed } = JSON.parse(event.body) // verify the request body. if (!id) throw new Error(`Invalid input: missing key 'id'`) if (!name) throw new Error(`Invalid input: missing key 'name'`) if (!completed) throw new Error(`Invalid input: missing key 'completed'`) // get old todoInfo. const { Item } = await dynamo.send(new GetCommand({ TableName: tableName, Key: { id } })) // updateTodo. const param = { ...Item, name, completed, updatedAt: new Date().toISOString() } await dynamo.send(new PutCommand({TableName: tableName,Item: param,})); const result = { success: true, message: `OK`, value: param } body = result } catch (err) { statusCode = 400; body = err.message ?? err; } finally { body = JSON.stringify(body); } return { statusCode, body, headers, }; }; ``` ### removeTodo ```javascript= import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({}); const dynamo = DynamoDBDocumentClient.from(client); const tableName = "todo"; export const handler = async (event, context) => { let body; let statusCode = 200; const headers = { "Content-Type": "application/json", }; try { const { id } = event.pathParameters // removeTodo. await dynamo.send(new DeleteCommand({ TableName: tableName, Key: { id } })); const result = { success: true, message: `OK` } body = result } catch (err) { statusCode = 400; body = err.message ?? err; } finally { body = JSON.stringify(body); } return { statusCode, body, headers, }; }; ``` ## 4. 建立 API Gateway 到這邊我們已經寫完 lambda 了,現在我們要透過 API Gateway 接收 request 並proxy 到 lambda。 首先,我們打開 API Gateway,可以看到多種 API 可以選擇,可以根據需要選擇你的 API type,本篇選擇HTTP API。 參考資料:[在REST API 與 HTTP API 之間選擇](https://docs.aws.amazon.com/zh_tw/apigateway/latest/developerguide/http-api-vs-rest.html) ### 如何建立API 1. **點選 ```build``` 來建立 API** 2. **API Name 輸入 todo 並一路下一頁到最後點選 ```Create```** 3. **建立 Routes:** 點選 ```Routes``` 並點選 ```Create``` 後,可以看到以下畫面,我們選 POST 並輸入 /api/todo,接著點選 ```Create``` ![](https://i.imgur.com/67uflfS.png) 4. **附加 Integration:** 點選 ```Integrations``` 然後我們點選剛剛建立的```Route```,然後在 target 選擇 Lambda ,並選擇剛剛寫好的 Lambda function,接著送出。 ![](https://i.imgur.com/HUsMxV8.png) 接著我們重複步驟3、4,將前面所有 Lambda function 綁到相應的 route 吧! ## 現在,我們來打打看 API 吧 ! ![](https://i.imgur.com/FAia7sy.png)