iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
0
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 25

[今晚我想來點 Express 佐 MVC 分層架構] DAY 25 - 玩轉 DTO 與 ResponseObject

不曉得有沒有人會覺得很困惑,上一篇出現了 JWTPayloadDTO,但使用的時機點是在 Request 的時候,而不是 Response,這是什麼原因呢?讓我們繼續看下去!

DTO 用在哪裡?

在介紹 Controller 的時候有提到使用 DTO 將資料過濾,並回傳給客戶端,不過那只是 單向 的,根據 DTO 的定義,它是傳輸中的物件,表示 從客戶端來的資料也可以是 DTO ,又因為包裝成 DTO,我們更容易知道傳進來的參數有哪些。

重新設計 DTO

由於 Request 與 Response 都可能有 DTO,命名就顯得格外重要,不然很多 DTO 名字又難以辨別的時候,真的會很痛苦 QQ,所以我們先調整 DTOBase,將名稱改為 ResponseDTOBase

export class ResponseDTOBase {

  public readonly _id!: string;
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  constructor(dto: ResponseDTOBase) {
    this._id = dto._id;
    this.createdAt = dto.createdAt;
    this.updatedAt = dto.updatedAt;
  }

}

接著去修改 TodoDTO,將名稱改為 ResponseTodoDTO

import { ResponseDTOBase } from '../bases/dto.base';
import { TodoDocument } from '../models/todo.model';

export class ResponseTodoDTO extends ResponseDTOBase {

  public readonly content!: string;
  public readonly completed!: boolean;

  constructor(doc: TodoDocument) {
    super(doc);
    this.content = doc.content;
    this.completed = doc.completed;
  }

}

記得要把所有與 import 它們的部分都改名喔!

Response 型別問題

我們的 Controller 回傳型別都是 Promise<ResponseObject>,這樣其實沒有辦法一眼看出 ResponseObject 中的資料是什麼型別,我們可以透過設計泛型來解決這個問題,修改 ResponseObject

import { HttpStatus } from '../../types/response.type';

export class ResponseObject<T> {

  public readonly status: HttpStatus = HttpStatus.INTERNAL_ERROR;
  public readonly message: string = '';
  public readonly data: any = null;

  constructor(options: { status?: HttpStatus, message?: string, data?: T } = {}) {
    this.status = options.status || this.status;
    this.message = options.message || this.message;
    this.data = options.data || this.data;
  }
}

這時候很多檔案都會發生錯誤,因為要去指定 ResponseObjectdata 為何種型別,需要修改的項目如下:

修改 RouteBase

由於 responseHandler 會處理到各式各樣的資料型別,所以這裡直接用 any

  protected responseHandler(
    method: (
      req: Request, res: Response, next: NextFunction
    ) => Promise<ResponseObject<any>>, controller = this.controller) {
      return (req: Request, res: Response, next: NextFunction) => {
        method.call(this.controller, req, res, next)
          .then(obj => res.status(obj.status).json(obj))
          .catch((err: Error) => next(controller.formatResponse(err.message, (err as any).status || HttpStatus.INTERNAL_ERROR)));
      };
  }

修改 ControllerBase

RouteBase 相同,會處理各式各樣的資料型別,所以使用 any

import { HttpStatus } from '../types/response.type';
import { ResponseObject } from '../common/response/response.object';

export abstract class ControllerBase {

  public formatResponse(
    data: any,
    status = HttpStatus.INTERNAL_ERROR
  ): ResponseObject<any> {
    const options: any = { status };

    status >= 400
    ? options.message = data
    : options.data = data;

    const responseObject = new ResponseObject(options);

    return responseObject;
  }

}

修改 TodoController

將回傳的型別帶入 ResponseObject

import { Request } from 'express';

import { ControllerBase } from '../../../bases/controller.base';

import { TodoService } from './todo.service';

import { JWTPayloadDTO } from '../../../dtos/jwt-payload.dto';
import { ResponseTodoDTO } from '../../../dtos/todo.dto';
import { ResponseObject } from '../../../common/response/response.object';

import { HttpStatus } from '../../../types/response.type';

export class TodoController extends ControllerBase {

  private readonly todoSvc = new TodoService();

  public async getTodos(req: Request): Promise<ResponseObject<ResponseTodoDTO[]>> {
    const { limit, skip } = req.query;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dtos = await this.todoSvc.getTodos(payload, Number(limit), Number(skip));
    return this.formatResponse(dtos, HttpStatus.OK);
  }

  public async getTodo(req: Request): Promise<ResponseObject<ResponseTodoDTO>> {
    const { id } = req.params;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.getTodo(payload, id);
    if (!dto) {
      return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
    }
    return this.formatResponse(dto, HttpStatus.OK);
  }

  public async addTodo(req: Request): Promise<ResponseObject<ResponseTodoDTO>> {
    const { content } = req.body;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.addTodo(payload, content);
    return this.formatResponse(dto, HttpStatus.CREATED);
  }

  public async completedTodo(req: Request): Promise<ResponseObject<ResponseTodoDTO>> {
    const { id } = req.params;
    const { completed } = req.body;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.completedTodo(payload, id, completed);
    if (!dto) {
      return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
    }
    return this.formatResponse(dto, HttpStatus.OK);
  }

  public async removeTodo(req: Request): Promise<ResponseObject<null>> {
    const { id } = req.params;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.removeTodo(payload, id);
    if (!dto) {
      return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
    }
    return this.formatResponse(null, HttpStatus.NO_CONTENT);
  }

}

修改 LocalAuthController

將回傳的型別帶入 ResponseObject

import { Request, Response, NextFunction } from 'express';
import passport from 'passport';

import { ControllerBase } from '../../../bases/controller.base';

import { LocalAuthService } from './local-auth.service';

import { ResponseObject } from '../../../common/response/response.object';

import { HttpStatus } from '../../../types/response.type';


export class LocalAuthController extends ControllerBase {

  protected readonly localAuthSvc = new LocalAuthService();

  public async signup(req: Request): Promise<ResponseObject<string>> {
    const { username, password, email } = req.body;
    const user = await this.localAuthSvc.addUser(username, password, email);
    const token = this.localAuthSvc.generateJWT(user);
    return this.formatResponse(token, HttpStatus.CREATED);
  }

  public async signin(req: Request, res: Response, next: NextFunction): Promise<ResponseObject<string>> {
    passport.use(this.localAuthSvc.Strategy);
    const token = await this.localAuthSvc.authenticate(req, res, next);
    return this.formatResponse(token, HttpStatus.OK);
  }

}

小結

善用 DTO 可以讓 Request 與 Response 的參數更加透明,再搭配 ResponseObject 與泛型能使系統變得更好維護,不過也要注意過多的 DTO 會難以維護的問題,所以我建議 資料格式較複雜的時候再定義 即可。說到複雜的資料格式,就會想到表單驗證,有一種設計模式非常適合用在驗證,下一篇將會介紹該設計模式 - Pipe Pattern


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 24 - 實作會員關聯資料
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 26 - Validator 與 Pipe
系列文
今晚我想來點 Express 佐 MVC 分層架構30

尚未有邦友留言

立即登入留言