API Service Controller - Quản lý toàn bộ request lên server
easy to write, easy to read, easy to maintain
CHÚ Ý
Để cách dùng tốt hơn, mình đã viết thành một package trên npm
npm i easy-control-api
// or
yarn add easy-control-api
Ý tưởng
Mọi request, mọi config (headers, handle errors...) ta sẽ xử lý tập trung và component chỉ việc lấy ra và sử dụng. Sau đó trả về kết quả mong muốn, component không cần biết quá trình xử lý như thế nào.
Nào bắt đầu thôi
Để hiểu đọc hiểu được bài viết này bạn cần ít nhất trang bị kiến thức về JS và một chút OOP. Mình sẽ lợi dụng tính chất bao đóng của OOP để gói gọn lại toàn bộ những biến, phương thức liên quan vào một class
Xây dựng class API Service Controller
Chuẩn bị các type
// type cho header object
export type ApiHeader = { [x: string]: string };
// type cho các method mà API Service Controller hỗ trợ
export type ApiMethod = "POST" | "GET" | "PUT" | "DELETE";
Khai báo các biến và getter/setter
export class APIService {
/**
* @memberof APIService
*
* Methods
*/
private method: ApiMethod = 'GET';
/**
* @memberof APIService
*
* Headers's request
*/
private headers: ApiHeader = {};
/**
* @memberof APIService
*
* Error handler for error requests
*/
static errorHandler: any = null;
/**
* @memberof APIService
*
* Handle loading for wait response
*/
static loadingHandler: any = null;
/**
* @memberof APIService
*
* Authentication token
*/
static token: string = '';
/**
* @memberof APIService
*
* url base - host's server
*/
static urlBase: string = 'http://localhost:3000';
set token(t: string) {
this.token = t;
}
set urlBase(url: string) {
this.urlBase = url;
}
set errorHandler(_errHandle: any) {
this.errorHandler = _errHandle;
}
public setHeaders(headers: ApiHeader) {
this.headers = headers;
return this;
}
public setMethod(_method: ApiMethod) {
this.method = _method;
return this;
}
private _callErrorHandler(msg: string) {
if (APIService.errorHandler) {
APIService.errorHandler(msg);
}
return this;
}
private _callLoadingHandler() {
if (APIService.loadingHandler) {
APIService.loadingHandler();
}
return this;
}
}
Có một lưu ý nho nhỏ, mình lưu token vào biến token nhưng thực tế các bạn có thể lưu chúng vào localStorage hoặc cookie nhé
Ý nghĩa của từng biến mình đã ghi rõ trong snippet ở trên.
Mình sẽ giải thích tại sao lại dùng private
và static
với các biến tương ứng
- Đầu tiên mình có 3 biến
private
làmethod, headers, errorHandler
. Đây là biến của thực thể, nghĩa là khi mà một thực thể tạo được tạo ra từ class củaAPIService
sẽ có những thuộc tính là như thế. Giả sử sau này mình có mộtclass Post
(extends từAPIService
) chuyên handle các request liên quan đếnPost
thì chúng sẽ có nhữngmethod, headers, errorHandler
riêng - Kế đó là 2 biến kiểu
static
làtoken
vàurlBase
, chúng là biến của lớp, sẽ xuyên suốt các object được tạo ra bởiAPIService
cũng như các class được extends từAPIService
. Hay nói cách khác nó biến dùng chung
Các bạn có thể đọc thêm ở đây
Tạo ra các request bằng axios
export class APIService {
// other lines code
/**
* determine method use by axios
*
* @param slug sub url to server
* @param body mothods PUT, POST.. need body
*/
private async _fetch<RequestBody>(slug: string, body?: RequestBody) {
const END_POINT = `${APIService.urlBase}${slug}`;
const axiosConfig: AxiosRequestConfig = {};
const token = APIService.token;
if (token) {
axiosConfig.headers = {
Authorization: `Bearer ${token}`,
};
}
// nếu header tồn tại
if (Object.keys(this.headers).length > 0 && this.headers.constructor === Object) {
axiosConfig.headers = {
...axiosConfig.headers,
...this.headers,
};
}
switch (this.method) {
case 'GET':
return axios.get<RequestBody>(END_POINT, axiosConfig);
case 'POST':
return axios.post<RequestBody>(END_POINT, body, axiosConfig);
case 'DELETE':
return axios.delete<RequestBody>(END_POINT, axiosConfig);
case 'PUT':
return axios.put<RequestBody>(END_POINT, body, axiosConfig);
default:
throw new Error('Method is not support or is invalid');
}
}
}
RequestBody
là generic type, type safe cho body gửi lên (nếu bạn chưa hiểu về generic thì lên google tìm hiểu nha). Còn nếu biết về generic nhưng nhìn hơi loạn hãy xem link git mình để cuối bài nhé
Đến hàm này chúng ta sẽ dựa vào APIService.urlBase
để ghép vào slug
để tạo ra một endpoint hoàn chỉnh.
Check token cũng như headers (nếu có) để gửi lên server. Cuối cùng ta dựa vào this.method
để để xem method nào sẽ được gửi lên
Handle results/error
export class APIService {
// other lines code
/**
* Parse result from ```thís._fetch()```
*
* You can use alter to show message error
*
* Except error > 499 need show page error (or something else) because is fatal error
*
* @param result
*/
private async _parseResult<ResponseBody>(
result: AxiosResponse<ResponseBody> | { status: number, data: any} | null,
) {
if (!result || !result.status) {
return {
success: false,
error: 'error:network_error',
};
}
let msg = result.data.message;
if (result.status > 499) {
msg = msg || 'error:server_error';
this._callErrorHandler(msg);
return {
success: false,
error: msg,
};
}
if (result.status === 401) {
return {
success: false,
error: msg || 'error:unauthorized',
};
}
if (result.status > 399) {
return {
success: false,
error: msg || 'error:generic',
};
}
if (result.status > 199) {
if (result.data.token) {
APIService.token = result.data.token;
}
return {
success: true,
data: result.data as ResponseBody,
};
}
return {
success: false,
error: msg || 'error:generic',
};
}
}
ResponseBody
là generic type, type safe cho kết quả nhận về (nếu bạn chưa hiểu về generic thì lên google tìm hiểu nha). Còn nếu biết về generic nhưng nhìn hơi loạn hãy xem link git mình để cuối bài nhé
Hàm này sẽ check status trả về và return lại message cũng như data nếu thành công. Xem thêm về HTTP response status codes
Request lên server
Chúng ta đã chuẩn bị hàm this._fetch
để lấy phương thức axios
tương ứng cùng với đó là hàm this._parseResult
, phân tích dữ liệu đã trả về. Giờ gộp chúng lại ta sẽ có một phương thức hoàn chỉnh
export class APIService {
// other lines code
/**
* Sending request to server
*
* Support generic type for ```ResponseBody``` and ```RequestBody```
*
* @description
* - ```ResponseBody```: type safe for response receive from server
* - ```RequestBody```: type safe for request body send to server
*
* @param slug
* @param body
*/
public async request<
ResponseBody = any,
RequestBody = any,
>(slug: string, body?: RequestBody, isLoading?: boolean) {
try {
isLoading && this._callLoadingHandler();
const result = await this._fetch<RequestBody>(slug, body);
this.setHeaders({}); // reset header sau khi gửi request
isLoading && this._callLoadingHandler();
return this._parseResult<ResponseBody>(result);
} catch (error) {
if (error.response && error.response.status) {
return this._parseResult({
status: error.response.status,
data: error.response.data as ResponseBody,
});
}
return this._parseResult<ResponseBody>(null);
}
}
Vậy là chúng ta đã xong phương thức chính dùng để request
lên server. Vậy làm sao để sử dụng class này?
Áp dụng APIService vào thực tế
Xây dựng class Post
Mình sẽ xây dựng class Post
, extends từ class APIService
. Nó sẽ chuyên handle các request liên quan đến post
export class Post extends APIService {
static slug = '/posts';
set slug (_url: string) {
Post.slug = _url;
}
get slug() {
return Post.slug;
}
public async getPosts() {
const res = await this
.setMethod('GET')
.request(Post.slug);
return res;
}
public async getPost(id: number) {
const res = await this
.setMethod('GET')
.request<IResponseBodyPost>(`${Post.slug}/${id}`);
return res;
}
public async createPost(post: IRequestBodyPost) {
const res = await this
.setHeaders({
test: 'this is a header'
})
.setMethod('POST')
.request<IResponseBodyPost, IRequestBodyPost>(Post.slug, post)
return res;
}
}
Kế đó tạo một thực thể dựa trên class này. Thực thể này sẽ được dùng ở component cần request dữ liệu lên server
export const PostApi = new Post();
Xây dựng component
import React, { useCallback, useState, useEffect } from 'react';
import { PostApi, APIService } from '../API/Post';
import { DisplayResult } from './displayResults';
export const GetPosts: React.FC = () => {
const [res, setRes] = useState('');
useEffect(() => {
// update urlBase cho APIService
APIService.urlBase = 'https://jsonplaceholder.typicode.com';
APIService.token = 'SOME_SECRET'
}, [])
const handlegetPosts = useCallback(async () => {
const _res = await PostApi.getPosts(); // get all posts
setRes(JSON.stringify(_res, null, 2))
}, [])
const handlegetPost = useCallback(async (id: number) => {
const _res = await PostApi.getPost(id); // get specified post
setRes(JSON.stringify(_res, null, 2))
}, [])
const handleCreatePost = useCallback(async () => {
// create new post
const _res = await PostApi.createPost({
title: 'create new blog with API Service Controller',
body: 'Dummy code...',
userId: 'Mason Nguyen',
});
setRes(JSON.stringify(_res, null, 2))
}, [])
const clearPost = useCallback(() => setRes(''),[])
return (
<>
<div>
<button onClick={handlegetPosts}>Get posts</button>
<button onClick={handleCreatePost}>Create posts</button>
<button onClick={() => handlegetPost(1)}>Get post specified (edit in code)</button>
<button onClick={clearPost}>Clear posts</button>
</div>
<DisplayResult txt={res} />
</>
)
}
XONG. Bạn nhìn thấy thật đơn giản phải không? 3 request nhưng nhìn rất ngắn, rất dễ đọc
Source code
Ở trên mình đa viết component getPost
để handle request đến post, ngoài ra trong code của mình còn có component getTodos
, các bạn tham khảo ở github của mình
Kết luận
Việc kiểm soát các request code tốt và tập trung một chỗ kiểu này sẽ giúp chúng ta maintain code tốt hơn, dễ đọc hơn và thêm được nhiều tuỳ biến chung hoặc riêng nhờ vào tính chất của class.
Comments