创建Express.js控制器的首选方法

113 阅读6分钟

Express.js是我最喜欢的网络框架,它与Node.js一起使用,用于在我的后端应用中构建RESTful API

Express.js如此受欢迎的原因之一是,它是最小的,允许你快速启动和运行,几乎没有模板代码。

由于对如何设置Express.js控制器和路由缺乏共识,因此很容易导致写出的代码不符合DRY原则,也不太一致。

保持我们的代码DRY以防止引入bug不仅是重要的,而且我们应该致力于保持我们的API响应一致。最好的开发者使用RESTful API的经验(想想Stripe的API)来自清晰和一致的RESTful响应。

一开始,我就想到了两种方法,我们可以提高响应的稳定性/结构性。

  1. 使用数据传输对象(DTOs)为响应对象的结构组成一个契约(一致的数据契约)。
  2. 使用一个BaseController来封装所有具体控制器的所有响应。

我们将在本文中讨论#2,并在另一篇文章中重新审视#1。

创建一个BaseController

我们希望创建某种BaseController ,代表一个控制器可以完成的所有功能,从一个地方。

例如,每个控制器都应该能够。

  • 接收一个请求和一个响应
  • 返回一个200,带有响应的有效载荷/dto
  • 返回一个200/201,没有响应的有效载荷/dto
  • 返回一个400错误
  • 返回一个500错误

为了封装所有这些功能,我们可以使用一个abstract 类。

shared/infra/http/models/BaseController.ts

import * as express from 'express'

export abstract class BaseController {

  /**
   * This is the implementation that we will leave to the
   * subclasses to figure out. 
   */

  protected abstract executeImpl (
    req: express.Request, res: express.Response
  ): Promise<void | any>;

  /**
   * This is what we will call on the route handler.
   * We also make sure to catch any uncaught errors in the
   * implementation.
   */

  public async execute (
    req: express.Request, res: express.Response
  ): Promise<void> {

    try {
      await this.executeImpl(req, res);
    } catch (err) {
      console.log(`[BaseController]: Uncaught controller error`);
      console.log(err);
      this.fail(res, 'An unexpected error occurred')
    }
  }

  public static jsonResponse (
    res: express.Response, code: number, message: string
  ) {
    return res.status(code).json({ message })
  }

  public ok<T> (res: express.Response, dto?: T) {
    if (!!dto) {
      res.type('application/json');
      return res.status(200).json(dto);
    } else {
      return res.sendStatus(200);
    }
  }

  public created (res: express.Response) {
    return res.sendStatus(201);
  }

  public clientError (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 400, message ? message : 'Unauthorized');
  }

  public unauthorized (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 401, message ? message : 'Unauthorized');
  }

  public paymentRequired (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 402, message ? message : 'Payment required');
  }

  public forbidden (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 403, message ? message : 'Forbidden');
  }

  public notFound (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 404, message ? message : 'Not found');
  }

  public conflict (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 409, message ? message : 'Conflict');
  }

  public tooMany (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 429, message ? message : 'Too many requests');
  }

  public todo (res: express.Response) {
    return BaseController.jsonResponse(res, 400, 'TODO');
  }

  public fail (res: express.Response, error: Error | string) {
    console.log(error);
    return res.status(500).json({
      message: error.toString()
    })
  }
}

在这一点上,你可能会问为什么我们除了有一个execute(req, res) 方法外,还有一个executeImpl(req, res)

我们的想法是,execute(req, res) 公共方法的存在是为了实际地Express handler与Router挂钩,而executeImpl(res, res) 方法则负责运行控制器的逻辑。

这样做是为了将请求和响应对象封装到控制器的状态中,并消除我们手动传递它们的需要。

如果你仍然感到困惑,请继续关注。随着我们的继续,它应该变得很清楚!

实现一个控制器

让我们举个基本的例子,创建一个User ,需要一个有效的passwordusernameemail

开始一个简单的控制器来创建一个用户,可能开始是这样的。

users/useCases/createUser/CreateUserController.ts

import * as express from 'express'

export class CreateUserController extends BaseController {
  protected async executeImpl (req: express.Request, res: express.Response): Promise<void | any> {
    try {
      // ... Handle request by creating objects
 
    } catch (err) {
      return this.fail(err.toString())
    }
  }
}

另外,看看那漂亮的自动补全和所有我们现在可以使用的方法,我们不再需要手动实现了!

实现抽象方法

当我们扩展一个abstract 类时,如果抽象类上有任何抽象方法,我们就需要在子类中实现这些方法。

请注意,我们已经在这个CreateUserController 中实现了。如果你记得在BaseController ,它有一个单一的abstract method

// Abstract method from the CreateUserController 
protected abstract executeImpl (req: express.Request, res: express.Response): Promise<void | any>;

这就是我们将定义控制器逻辑的地方。我们将从验证请求的有效载荷开始。

验证请求的有效载荷

让我们继续利用价值对象来验证username,passwordemail 从互联网上传来的信息是否有效。

users/useCases/createUser/CreateUserController.ts

import * as express from 'express'

export class CreateUserController extends BaseController {
  protected executeImpl (req: express.Request, res: express.Response): void {
    try {
      const { username, password, email } = req.body;
      const usernameOrError: Result<Username> = Username.create(username);
      const passwordOrError: Result<Password> = Password.create(password);
      const emailOrError: Result<Email> = Email.create(email);

      const result = Result.combine([ 
        usernameOrError, passwordOrError, emailOrError 
      ]);

      if (result.isFailure) {
        // Send back a 400 client error
        return this.clientError(res, result.error);
      }

      // ... continue

    } catch (err) {
      return this.fail(res, err.toString())
    }
  }
}

在这些价值对象类中,你可以使用Joi验证器、自定义验证方法和其他任何你喜欢的方法的组合,以确保价值对象的不变量得到满足。

然而,你可能会在这里看到一些东西。

结果类

现在,我们只需了解结果类是处理错误的一种更简洁的方式,而不是明确地抛出错误。它还允许我们按顺序将combine(Result[]?),并将返回所提供的数组中第一个无效的结果。

这反过来又有助于将真正有用的上下文错误信息传递给使用该API的客户端。


完成了它

为了完成这个API请求,我们要将User ,并持久化。

为了做到这一点,我们将确保利用Depdendency Inversion来指定这个类依赖于一个IUserRepo

用户/useCases/createUser/CreateUserController.ts

import * as express from 'express'

export class CreateUserController extends BaseController {
  private userRepo: IUserRepo;

  constructor (userRepo: IUserRepo) {
    super();
    this.userRepo = userRepo;
  }

  protected async executeImpl (req: express.Request, res: express.Response): Promise<void | any> {
    try {
      const { username, password, email } = this.req.body;
      const usernameOrError: Result<Username> = Username.create(username);
      const passwordOrError: Result<Password> = Password.create(password);
      const emailOrError: Result<Email> = Email.create(email);

      const result = Result.combine([ 
        usernameOrError, passwordOrError, emailOrError 
      ]);

      if (result.isFailure) {
        // Send back a 400 client error
        return this.clientError(res, result.error);
      }

      // ... continue
      const userOrError: Result<User> = User.create({
        username: usernameOrError.getValue(),
        password: passwordOrError.getValue(),
        email: emailOrError.getValue()
      });

      if (userOrError.isFailure) {
        // Send back a 400 client error
        return this.clientError(res, result.error);
      }

      const user: User = userOrError.getValue();

      // Create the user
      await this.userRepo.createUser(user);

      // Return a 200
      return this.ok<any>(res);

    } catch (err) {
      return this.fail(res, err.toString())
    }
  }
}

这就是控制器的内容!

将其与Express.js路由相连接

为了从一个模块导出功能而不需要DI容器,我喜欢这样做。

users/useCases/createUser/index.ts

import { CreateUserController } from './CreateUserController'
import { UserRepo } from '../repos/UserRepo';
import { models } from '../infra/sequelize';

const userRepo = new UserRepo(models);
const createUserController = new CreateUserController(userRepo);

// Export the feature 
export { createUserController };

为了连接它,我们可以创建一个单独的路由器,添加任何我们需要的中间件(例如这里显示了两个),然后像这样执行控制器。

import { createUserController } from '../useCases/createUser'

import * as express from 'express'
import { Router } from 'express'
const userRouter: Router = Router();

userRouter.post('/new', 
  middleware.useCORS,
  middleware.rateLimit,
  // + any other middleware 
  ...
  (req, res) => createUserController.execute(req, res)
);

export { userRouter }

最后,我们可以从我们的主Express.js应用实例中导入它。

// app.js

import { userRouter } from '../users/http/routers'

const app = express();
app.use('/user', userRouter)

然后就可以了!POST/user/new 应该就可以了。


现在你知道了我创建Express.js控制器的首选方法,即在一个抽象类中封装所有的通用功能。

这种类型的东西也可以不用TypeScript来做,但它需要更多的技巧,以实现我们在本文中提到的设计模式和原则。