# API串接文件
## 環境配置(確認以下檔案)
### .env.development(建立在主資料夾下,與.gitignore同層)
```
NEXT_PUBLIC_API_URL=後端IP
NEXTAUTH_URL=前端IP
```
關於環境檔設定可以參考以下連結:
https://hackmd.io/@ntubimdbirc/BJNlGVcha#env-%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A%E6%AA%94%E6%A1%88
### api.ts(建立在libs/api/)
```
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
const API = axios.create({ 'baseURL': process.env.NEXT_PUBLIC_API_URL });
API.interceptors.request.use(function (config: InternalAxiosRequestConfig) {
if (!config.headers.has('Content-Type')) {
config.headers.set('Content-Type', 'application/json');
}
if (!config.headers['Content-Type']) {
config.headers['Content-Type'] = 'application/json';
}
const token = sessionStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
API.interceptors.response.use(
async (response: AxiosResponse) => {
if (!response.data.result) {
throw response.data;
}
const newToken = response.headers['x-auth-token'];
if (newToken) {
sessionStorage.setItem('token', newToken);
}
return response.data;
},
error => Promise.reject(error?.response?.data || error)
);
export default API;
```
> 基本上不會有變動,只需要從舊專案撈檔案貼上就好
> 只是要注意此專案token存在sessionStorage還是localStorage
> 關於兩者差異可以參考以下連結:
> https://www.notion.so/1abd1c3e439680dc98dfc72f320c36bf
### requestType.ts(建立在libs/types/)
```
export type Response<T> = {
success: boolean;
result: boolean;
errorCode: string;
message?: string;
data?: T;
};
```
> 主要用於前端接收後端回傳的統一 JSON 資料格式。
> 基本上也都會長一樣,除非後端改了回傳的格式
## 介紹
在後端開發中,通常會使用 API 路由來分類與管理各項功能,讓整體架構清晰一點。
### API路由
API 路由類似於一種分門別類的網址設計規則。例如:
與「使用者」相關的功能,API 通常會以 /user 開頭,表示這組 API 都是處理使用者資料的。
再加上 RESTful API 的設計風格,常見的 CRUD 操作會對應特定的 HTTP 請求方法,這樣就能不用再額外命名動作,而是透過「動詞(請求方法)」與「路徑」來表達功能。
註:
CRUD 是指資料處理的四種基本操作:
Create(新增)Read(查詢)Update(更新)Delete(刪除)
HTTP 請求方法 是前端對後端發送資料操作的方式,常對應 CRUD 操作如下:
POST 對應 Create, GET 對應 Read, PATCH 對應 Update, DELETE 對應 Delete
範例:
GET /user 查詢所有使用者
POST /user 新增使用者
PATCH /user/{id} 更新使用者
DELETE /user/{id} 刪除使用者
## 實做(以下用member為範例)
首先在libs/api/ 中建立你要串接的API"分類"檔案,例如要串接"成員"分類的則可命名為memberAPI.ts(API皆為大寫,副檔名.ts)
則基本檔案建立會如下:
```
import API from '@/libs/api/api';
import { Response } from '@/libs/types/requestType';
const BASE_URL = 'member'; //路由分類(同一個分類回放在同一個.ts檔)
const memberAPI = {
};
export default memberAPI;
```
---
### 查詢清單(全查)
後端API路由為 /member,要注意因為是全查,因此接收的為陣列[ ]。
```
import API from '@/libs/api/api';
import { Response } from '@/libs/types/requestType';
const BASE_URL = 'member';
const memberAPI = {
'searchMember': (): Promise<Response<MemberType[]>> =>
API.get(BASE_URL),
};
export default memberAPI;
```
---
> 建立指定資料型別(/libs/type/memberType.ts)
> 根據後端Swagger資料得出,如果不清楚可以詢問後端。
```
export type memberType = {
memberId: string,
name: string,
gradeId:number,
grade:string,
email:string,
gender:string,
filePath:string | null,
}
```
---
index.tsx
這邊useState接收的一樣是陣列[ ],useEffect可以在該page一開啟時就呼叫{ }內的函式
```
const [memberList, setMemberList] = useState<memberType[]>();
const searchMember = async () => {
try {
const response = await memberAPI.searchMember();
setMemberList(response.data);
} catch (error: any) {
alert(error.message)
}
};
useEffect(() => {
searchMember();
}, []);
```
---
#### 說明
searchMember 是自訂的 API 方法,用來包裝對 /member 路徑的 GET 請求。
呼叫 memberAPI.searchMember() 時,會透過 Axios 發送請求,並回傳一個符合 Response< memberType> 結構的 Promise。
---
### 新增
新增資料理所當然的要傳參數給後端,傳參數有幾種常見的方式
1. path Parameters ,例如/api/users/{id}
2. query Parameters ,例如/api/users?name=John
3. form-data,例如
```
const formData = new FormData();
formData.append('name', 'John');
formData.append('age', '18');
```
4. Request body (json),例如
```
{
'name':'John',
'age': 18
}
```
註:form-data與json的區別
| 類型 | 可傳檔案 | 適合用途 | Content-Type
| --------- | ---- | ---------- | -------------------
| form-data | ✅ | 表單上傳 + 檔案 | multipart/form-data |
| JSON | ❌ | 純資料傳遞(無檔案) | application/json |
這邊範例使用form-data
memberType.ts
```
export type CreateMemberType = {
memberId: string,
name: string,
gradeId:number,
email:string,
gender:string
}
```
memberAPI.ts
```
const memberAPI = {
'createMember': (data: FormData): Promise<Response<any>> =>
API.post(`${BASE_URL}/create`, data, { 'headers': { 'Content-Type': 'multipart/form-data' } }),
};
```
index.tsx
> 使用 react-hook-form 的 useForm 函式,建立一個表單的控制器,並給予初始值。
```
const { handleSubmit, getValues, register, setValue, reset } = useForm<CreateMemberType>({
'defaultValues': {
'memberId': '',
'name': '',
'gender': '',
'email': '',
'gradeId': undefined
}
});
```
| 函式 | 說明 |
| -------------- | -------------------------------------------------------------------- |
| `handleSubmit` | 表單提交時使用的函式,用來處理驗證後的資料送出。<br>用法:`onSubmit={handleSubmit(submitFunction)}` |
| `getValues` | 取得當前所有欄位的值或單一欄位的值。 |
| `register` | 註冊每個 input 欄位,讓它能被 `react-hook-form` 控管。 |
| `setValue` | 手動設定某個欄位的值。 |
| `reset` | 重置整個表單(可帶新的預設值)。|
register,在每個填入的欄位都需要加上
```
<input
type="text"
placeholder="請輸入姓名"
{...register('name')}
/>
```
submitFunction
```
const submitFuction = async () => {
const params = getValues();
try {
const response = await memberAPI.createMember(params);
alert('新增成功' | response.message);
reset();
} catch (error: any) {
alert(error.message);
}
}
```
> 註:所有<input>欄位與 submit 按鈕應該放在<form></form> 中,才會觸發handleSubmit()
> 或者可以直接將handleSubmit()放在<form>標籤內
</form>
---
### 修改
> 通常修改的時候會先帶入原本的資料,因此會使用單筆查詢API獲得資料,再給予需要修改的值做更新。
#### 單筆查詢
```
getMember: (memberId: string): Promise<Response<memberType>> =>
API.get(`${BASE_URL}/${memberId}`),
```
setValue:手動設定某個欄位的值。
```
const getMember = async (memberId: string) => {
try {
const response = await memberAPI.getMember(memberId);
const memberData = response.data;
if (memberData) {
setValue('memberId', memberData.memberId);
setValue('name', memberData.name);
setValue('gradeId', memberData.gradeId);
setValue('email', memberData.email);
setValue('gender', memberData.gender === '男' ? '0' : '1');
// 簡化寫法(必須確保後端傳的參數,與欄位註冊名稱一樣)
// Object.entries(values).forEach(([key, value]) => {
// setValue(key as keyof FormValues, value);});
}
} catch (error: any) {
alert(error.message);
}
}
```
#### 修改
一樣使用form-data傳參數
```
'updateMember': (memberId: string, data: FormData): Promise<Response<any>> =>
API.patch(`${BASE_URL}/update/${memberId}`, data, { 'headers': { 'Content-Type': 'multipart/form-data' } }),
```
```
const updateMemberFunction = async () => {
const params = getValues();
try {
const response = await memberAPI.updateMember(params.memberId, params);
alert(response.message | '修改成功');
reset();
} catch (error: any) {
alert(error.message);
}
}
```
### 刪除
memberAPI.ts
> 刪除一般使用path Parameters傳入參數,ex.單筆編號
```
'deleteMember': (memberId: string): Promise<Response<any>> =>
API.delete(`${BASE_URL}/delete/${memberId}`)
```
index.tsx
```
const deleteMember = async (memberId: string) => {
try {
const response = await memberAPI.deleteMember(memberId);
alert('刪除成功');
} catch (error: any) {
alert(error.message);
}
}
```