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 privatestatic với các biến tương ứng

  • Đầu tiên mình có 3 biến privatemethod, 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ủa APIService sẽ có những thuộc tính là như thế. Giả sử sau này mình có một class Post (extends từ APIService) chuyên handle các request liên quan đến Post thì chúng sẽ có những method, headers, errorHandler riêng
  • Kế đó là 2 biến kiểu statictokenurlBase, chúng là biến của lớp, sẽ xuyên suốt các object được tạo ra bởi APIService 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

Contact for work:Skype
Code from my 💕