前面幾篇已經把 Model 、 View 、 Controller 都交代完畢了,現在可以將它們組裝起來,變成一個簡單的 TodoList API Server。這時候應該會有人感到困惑:什麼? View 去哪裡了?為什麼沒有看到?由於我們設計的是 API Server,所以 View 的部分就是 ResponseObject,但確實還沒有設計 DTO,那廢話不多說,直接進入 DTO!
先思考一下,我們在設計 Model 的時候,有抽離一個部分叫做 createdAt
與 updatedAt
,這個基本上會是回傳給客戶端的重要資訊之一,所以可以將這部分出離出來做成 Base,另外還有一個重要資訊就是資料的 id 欄位,也一同併入 Base 中。在 bases
資料夾下,新增 dto.base.ts
:
export class DTOBase {
public readonly _id!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
constructor(dto: DTOBase) {
this._id = dto._id;
this.createdAt = dto.createdAt;
this.updatedAt = dto.updatedAt;
}
}
接著,新增一個 dtos
資料夾並建立 todo.dto.ts
,其中傳入的參數為 TodoDocument
,因為資料會從 Document 來:
import { DTOBase } from '../bases/dto.base';
import { TodoDocument } from '../models/todo.model';
export class TodoDTO extends DTOBase {
public readonly content!: string;
public readonly completed!: boolean;
constructor(doc: TodoDocument) {
super(doc);
this.content = doc.content;
this.completed = doc.completed;
}
}
我們說 Controller 就是 View 與 Model 之間的橋樑,所以我們要在 Controller 進行整合,修改 todo.controller.ts
,設計 新增 Todo 、 取得 Todos 、 取得 Todo 、 完成 Todo 以及 刪除 Todo 的功能:
新增通常會使用 POST 方法,並會把相關資訊放在 body 中,所以要在 TodoController
中新增 addTodo
這個方法,並從 Request 的 body 獲得 Todo 的 content
,透過 TodoModel
進行新增,最後就是把存入的資訊用 TodoDTO
包裝起來,再傳入 ResponseObject
中並回傳給客戶端:
public async addTodo(req: Request): Promise<ResponseObject> {
const { content } = req.body;
const todo = new TodoModel({ content, completed: false });
const document = await todo.save();
const dto = new TodoDTO(document);
return this.formatResponse(dto, HttpStatus.CREATED);
}
修改 TodoRoute
:
protected registerRoute(): void {
this.router.route('/')
.post(
express.json(),
this.responseHandler(this.controller.addTodo)
);
}
透過 Postman 看結果:
取得通常會使用 GET 方法,並將查詢參數放在 QueryString 及 Parameters,我們在 TodoController
中新增 getTodos
這個方法,並從 QueryString 獲得 skip
與 limit
,以判斷要跳過幾筆、取幾筆資料,這裡需要特別注意的是,如果沒有帶參數的話應該給予預設值以及 limit
的上限,所以要在 types
資料夾下,新增 request.type.ts
,並使用 enum 來設置預設值:
export const enum DefaultQuery {
SKIP = 0,
LIMIT = 30,
MAX_LIMIT = 100
};
在 TodoController
中新增 getTodos
方法:
public async getTodos(req: Request): Promise<ResponseObject> {
const { limit, skip } = req.query;
const truthLimit = Math.min(Number(limit), DefaultQuery.MAX_LIMIT) || DefaultQuery.LIMIT;
const truthSkip = Number(skip) || DefaultQuery.SKIP;
const todos = await TodoModel.find().skip(truthSkip).limit(truthLimit);
const dtos = todos.map(todo => new TodoDTO(todo));
return this.formatResponse(dtos, HttpStatus.OK);
}
修改 TodoRoute
:
protected registerRoute(): void {
this.router.route('/')
.get(
this.responseHandler(this.controller.getTodos)
)
.post(
express.json(),
this.responseHandler(this.controller.addTodo)
);
}
透過 Postman 看結果:
TodoList 都會設置一個功能:當該筆項目完成的時候會勾選起來以表示完成,我們要來實作這個功能,這種操作行為屬於更新,所以使用 PATCH 方法,從 Request 的 body 獲得 Todo 的 completed
狀態,並從 Parameters 取得該筆 Todo 的 id。在 TodoController
新增 completedTodo
這個方法:
public async completedTodo(req: Request): Promise<ResponseObject> {
const { id } = req.params;
const { completed } = req.body;
const options: QueryFindOneAndUpdateOptions = {
new: true,
runValidators: true
};
const todo = await TodoModel.findByIdAndUpdate(id, { completed }, options);
if ( !todo ) {
return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
}
const dto = new TodoDTO(todo);
return this.formatResponse(dto, HttpStatus.OK);
}
修改 TodoRoute
:
protected registerRoute(): void {
this.router.route('/')
.get(
this.responseHandler(this.controller.getTodos)
)
.post(
express.json(),
this.responseHandler(this.controller.addTodo)
);
this.router.patch(
'/:id/completed',
express.json(),
this.responseHandler(this.controller.completedTodo)
);
}
透過 Postman 看結果:
刪除通常會使用 DELETE 方法,在 TodoController
新增 removeTodo
這個方法,並從 Parameters 取得 Todo 的 id 來進行刪除:
public async removeTodo(req: Request): Promise<ResponseObject> {
const { id } = req.params;
const todo = await TodoModel.findByIdAndRemove(id);
if ( !todo ) {
return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
}
return this.formatResponse(null, HttpStatus.NO_CONTENT);
}
修改 TodoRoute
:
protected registerRoute(): void {
this.router.route('/')
.get(
this.responseHandler(this.controller.getTodos)
)
.post(
express.json(),
this.responseHandler(this.controller.addTodo)
);
this.router.route('/:id')
.delete(
this.responseHandler(this.controller.removeTodo)
);
this.router.patch(
'/:id/completed',
express.json(),
this.responseHandler(this.controller.completedTodo)
);
}
透過 Postman 看結果:
與查詢 Todos 差不多,但沒有 skip
與 limit
,以及需要從 Parameters 取得 Todo 的 id,在 TodoController
新增 getTodo
這個方法:
public async getTodo(req: Request): Promise<ResponseObject> {
const { id } = req.params;
const todo = await TodoModel.findById(id);
if ( !todo ) {
return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
}
const dto = new TodoDTO(todo);
return this.formatResponse(dto, HttpStatus.OK);
}
修改 TodoRoute
:
protected registerRoute(): void {
this.router.route('/')
.get(
this.responseHandler(this.controller.getTodos)
)
.post(
express.json(),
this.responseHandler(this.controller.addTodo)
);
this.router.route('/:id')
.get(
this.responseHandler(this.controller.getTodo)
)
.delete(
this.responseHandler(this.controller.removeTodo)
);
this.router.patch(
'/:id/completed',
express.json(),
this.responseHandler(this.controller.completedTodo)
);
}
透過 Postman 看結果:
最後,來確認一下資料夾結構:
├── src
| ├── index.ts
| ├── app.ts
| ├── app.routing.ts
| ├── bases
| | ├── controller.base.ts
| | ├── route.base.ts
| | └── dto.base.ts //本篇新增
| ├── + common/resonse
| ├── + exceptions
| ├── main
| | └── api
| | ├── todo.controller.ts //本篇修改
| | └── todo.routing.ts //本篇修改
| ├── + models
| ├── dtos //本篇新增
| | └── todo.dto.ts //本篇新增
| ├── types
| | ├── model.type.ts
| | ├── response.type.ts
| | └── request.type.ts //本篇新增
| ├── + environments
| ├── + database
| └── + validators
├── package.json
└── tsconfig.json
今天終於完成了基本的 MVC 架構了,不曉得大家有沒有注意到這個架構還是有點美中不足,也就是前面我提到的 Controller 過於笨重 的問題,現在的時代我們通常會把 MVC 做點變化,主要就是希望分散每個單元的負擔,這樣比較易於擴充與修改,下一篇將會介紹把 Controller 邏輯抽離的設計模式 - Service Layer Pattern 。