不曉得有沒有人會覺得很困惑,上一篇出現了 JWTPayloadDTO
,但使用的時機點是在 Request 的時候,而不是 Response,這是什麼原因呢?讓我們繼續看下去!
在介紹 Controller 的時候有提到使用 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 它們的部分都改名喔!
我們的 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;
}
}
這時候很多檔案都會發生錯誤,因為要去指定 ResponseObject
的 data
為何種型別,需要修改的項目如下:
由於 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)));
};
}
與 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;
}
}
將回傳的型別帶入 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);
}
}
將回傳的型別帶入 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。