Express.js是我最喜欢的网络框架,它与Node.js一起使用,用于在我的后端应用中构建RESTful API。
Express.js如此受欢迎的原因之一是,它是最小的,允许你快速启动和运行,几乎没有模板代码。
由于对如何设置Express.js控制器和路由缺乏共识,因此很容易导致写出的代码不符合DRY原则,也不太一致。
保持我们的代码DRY以防止引入bug不仅是重要的,而且我们应该致力于保持我们的API响应一致。最好的开发者使用RESTful API的经验(想想Stripe的API)来自清晰和一致的RESTful响应。
一开始,我就想到了两种方法,我们可以提高响应的稳定性/结构性。
- 使用数据传输对象(DTOs)为响应对象的结构组成一个契约(一致的数据契约)。
- 使用一个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
,需要一个有效的password
、username
、email
。
开始一个简单的控制器来创建一个用户,可能开始是这样的。
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
,password
和email
从互联网上传来的信息是否有效。
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来做,但它需要更多的技巧,以实现我们在本文中提到的设计模式和原则。