Ứng dụng web sử dụng DynamoDB, Typescript, Express, Generic repository pattern…(Phần 07 – Phần cuối) – Movie api

Đây là bài viết cuối cùng của series bài viết này. Khi hoàn thành tới bài viết này, chúng ta đã có một ứng dụng gần như hoàn chỉnh, ứng dụng cung cấp api để lấy thông tin được lưu trong CSDL.

Nội dung của bài viết tập trung vào xây dựng “Movie controller”, trong bài viết chỉ hướng dẫn xây dựng một api –  GET /movies/:year để lấy thông tin các bộ phim trong 1 năm nhất định. Các bạn có thể dựa vào đó để phát triển tiếp các api khác.

Ứng dụng được viết theo luồng như sau: Các lớp đối tượng ở mức cao sẽ sử dụng các lớp đối tượng ở mức thấp hơn gần nhất. Việc phụ thuộc đảm bảo tính DI (dependency injection) thông qua truyền vào tham số ở constructor.

App <-- Controller <-- Service <-- Repository <-- Model <-- Database connection

Các bạn có thể viết theo hướng đi từ thấp tới cao, hay từ cao tới thấp đều được.

MovieService

Đến bài viết trước (Phần 05) chúng ta đã xây dựng được MovieRepository. Giờ chúng ta sẽ xây dựng tới MovieService.

Service class là nới kết hợp các Repository để tạo ra các hàm tiện ích, các hàm này sẽ được sử dụng ở Controller.

Service trong bài viết này khá đơn giản, chúng ta chỉ có một Repository, không cần kiểm tra dữ liệu đầu vào, không cần xử lý kết quả trả về…

src/modules/movie/MovieService.ts

import { Movie } from '../../entities/Movie';
import { MovieRepository } from '../../repositories/MovieRepository';
import { BaseService } from '../base/BaseService';

export class MovieService extends BaseService {
  constructor(
    private readonly movieRepo: MovieRepository,
  ) {
    super();
  }

  public getMoviesByYear(year: number): Promise<Movie[]> {
    return this.movieRepo.getByYear(year);
  }
}

BaseService hiện tại đang là một class trống, tại đây chúng ta có thể thêm vào các phương thức có thể được dùng chung ở các service như logging, translate…

Phương thức getMoviesByYear nhận vào một tham số là year, chuyển tiếp tham số này cho phương thức getByYear của Repository. Tại đây chúng ta có thể thêm các logic cần thiết khác như kiểm tra giới hạn max min của tham số year.

MovieController

Khi đã có lớp Service, giờ chúng ta xây dựng lớp Controller cho đối tượng Movie

src/modules/movie/MovieController.ts

import { Request } from 'express';
import { Movie } from '../../entities/Movie';
import { BaseController, ControllerRoute } from '../base/BaseController';
import { MovieService } from './MovieService';

export class MovieController extends BaseController {
  constructor(
    private readonly movieService: MovieService,
  ) {
    super();
    this.path = '/movies'
  }

  public initRoutes(): ControllerRoute[] {
    return [
      {
        method: 'get',
        path: '/:year',
        handler: this.getMoviesByYear,
      },
    ];
  }

  public getMoviesByYear(req: Request): Promise<Movie[]> {
    const year = parseInt(req.params.year, 10);
    return this.movieService.getMoviesByYear(year);
  }
}

Ở đây, chúng ta có this.path = '/movies'; để định nghĩa endpoint cho movie controller.

Với phương thức initRoutes, chúng ta đăng ký một route của controller này, tương ứng api GET /movies/:year sẽ được xử lý bằng phương thức getMoviesByYear.

Phương thức getMoviesByYear sẽ xử lý request từ phía client, tại phương thức này chúng ta lấy giá trị year từ đối tượng req (Request) rồi gọi tới phương thức của service, cuối cùng là trả về kết quả tương ứng.

App

Đây là nơi chúng ta đăng ký sử dụng controller, và tại đây chúng ta cũng khởi tạo các lớp phụ thuộc theo thứ tự từ lớp thấp tới lớp cao.

Chúng ta cập nhật vào file index.ts

src/index.ts

diff --git a/src/index.ts b/src/index.ts
index f231f9f..18b487b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,16 +3,30 @@
  * Created by SUN-ASTERISK\dinh.van.hoang on 2/24/20
  */
 
+import { DynamoDB } from 'aws-sdk';
 import express from 'express';
 import { ENVIRONMENTS } from './config';
 import { HomeController } from './modules/home/HomeController';
+import { MovieController } from './modules/movie/MovieController';
+import { MovieService } from './modules/movie/MovieService';
+import { MovieRepository } from './repositories/MovieRepository';
 import { App } from './server/App';
 
 (async () => {
+  const docClient = new DynamoDB.DocumentClient({
+    endpoint: ENVIRONMENTS.DYNAMO_ENDPOINT,
+    region: ENVIRONMENTS.REGION,
+  });
+
+  const movieRepo = new MovieRepository(docClient);
+  const movieService = new MovieService(movieRepo);
+
+
   const app = new App({
     port: ENVIRONMENTS.PORT,
     controllers: [
       new HomeController(),
+      new MovieController(movieService),
     ],
     middleware: [
       express.json(),

Ở đây, chúng ta khởi tạo lần lượt các đối tượng DocumentClient, MovieRepository, MovieService, cuối cùng là MovieController.

Chúng ta đăng ký MovieController cho đối tượng App, logic của đối tượng App sẽ load các route của MovieController.

Khi bạn chạy thử với lệnh npm run dev, chúng ta sẽ nhận được kết quả như sau:

Method    Path                Handler
GET       /                   HomeController.index
GET       /info               HomeController.hello
GET       /movies/:year       MovieController.getMoviesByYear
Server is running on http://localhost:8080

Giờ hãy thử gọi api để lấy thông tin các bộ phim của năm 2018

curl --location --request GET 'http://localhost:8080/movies/2018'

Kết quả:

[{"title":"Halloween III","year":2018,"info":{"rank":4896,"plot":"The plot is unknown at this time.","genres":["Horror"],"directors":["Patrick Lussier"]}}]

Kết luận

Chúng ta đã hoàn thành series, có thể tóm tắt lại nội dung như sau:

Series bài viết cố gắng truyền tải 1 cách để xây dựng một api service bằng express, sử dụng những tính năng của Typescript, Javascript để làm ứng dụng có khả năng mở rộng, dễ dàng bảo trì…

Các bạn có thể sử dụng những gợi ý của bài viết, code mẫu để có tự xây dựng được ứng dụng của riêng mình.

Đây là những gì được thêm vào, thay đổi so với Phần 06Commit

Đây là project sau khi kết thúc phần 07: Github