使用后台任务实现 API:一种实用的方法
API 是现代应用程序的支柱,但有时它们需要做的不仅仅是 CRUD 操作。
考虑这样一种情况:您需要更新用户的个人资料,同时还要高效地发送后台通知和电子邮件,并且不会阻止主要请求。
让我们来看看如何使用结构化、函数式编程启发的方法来实现这一点。
问题陈述
我们需要实现一个更新用户 API:
- 接受用户名或电话号码的更新请求。
- 标识已更改的字段。
- 相应地更新数据库。
- 获取用户的设备令牌并在后台发送通知。
- 异步向用户发送电子邮件。
- 向客户端返回适当的响应。
为什么采用功能性方法?
传奇游戏开发者约翰·卡马克 (John Carmack) 认为函数式编程可以减少副作用并提高软件可靠性。
他认为,软件开发中许多缺陷的出现都是因为程序员没有完全理解他们的代码可能执行的所有状态。这个问题在多线程环境中被放大,竞争条件可能导致不可预测的行为。
函数式编程通过明确状态并减少意外的副作用来缓解这些问题。
即使我们不使用 Haskell 或 Lisp,我们仍然可以在 JavaScript 和 TypeScript 等主流语言中应用函数式编程原理。
正如卡马克所说,“无论你使用什么语言,以函数式风格编程都会带来好处。”
通过使用纯函数、不变性和明确的关注点分离来设计我们的 API,我们提高了可测试性、可维护性和可扩展性。
设计 API
1. 路线定义
我们在后端框架(例如 Nest.js、Express 或 Django)中定义一个端点:
PATCH /api/user/update
2. 请求负载
请求中应该包含需要更新的字段:
{
// "userId": "12345",
"username": "newUser123",
"phoneNumber": "9876543210"
}
3. Node.js 中的实现(Nest.js 示例)
我们遵循结构化的方法来分离关注点并保持我们的功能可测试和可重用。
import { Controller, Patch, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
import { EmailService } from './email.service';
@Controller('user')
export class UserController {
constructor(
private readonly userService: UserService,
private readonly notificationService: NotificationService,
private readonly emailService: EmailService,
) {}
@Patch('update')
async updateUser(@Body() body: any) {
const { userId, username, phoneNumber } = body;
// Check what has changed
const updates: Partial<User> = {};
if (username) updates['username'] = username;
if (phoneNumber) updates['phoneNumber'] = phoneNumber;
// Update user in DB (Pure function approach)
const updatedUser = await this.userService.updateUser(userId, updates);
// Fetch user token and send notification (background)
this.notificationService.sendUserUpdateNotification(userId);
// Send email (background)
this.emailService.sendUpdateEmail(updatedUser);
return { message: 'User updated successfully', data: updatedUser };
}
}
4. 数据库更新函数(纯函数方法)
以下函数更新数据库并确保数据一致性:
async updateUser(userId: string, updates: Partial<User>) {
return this.userModel.findByIdAndUpdate(userId, updates, { new: true });
}
John Carmack 强调纯函数仅对其输入进行操作并返回计算值,而不会修改共享状态。这种方法可以确保:
- 线程安全:没有意外的副作用。
- 可重用性:易于移植到新的环境中。
- 可测试性:对于相同的输入始终返回相同的输出。
5. 后台通知处理
我们获取用户的令牌并发送推送通知,而不会阻止主请求。
async sendUserUpdateNotification(userId: string) {
const userDevice = await this.userDeviceModel.findOne({ userId });
if (userDevice?.token) {
pushNotificationService.send(userDevice.token, 'Your profile has been updated. If it was not done by you, please contact support.');
}
}
6. 在后台发送电子邮件
电子邮件的处理速度可能很慢,因此我们将其卸载到BullMQ / Kafka等工作队列中:
async sendUpdateEmail(user: User) {
emailQueue.add('sendEmail', {
to: user.email,
subject: 'Profile Updated',
body: 'Your profile has been successfully updated. If it was not done by you, please contact support.',
});
}
务实的平衡
并非所有事物都可以纯粹发挥功能——现实世界的应用程序需要与数据库、文件系统和外部服务进行交互。
正如卡马克所说,“在更广泛的背景下避免最坏的情况通常比在有限的情况下实现完美更为重要。”
我们的目标不是在所有地方强制执行严格的纯度,而是在以可控的方式处理必要的突变的同时,尽量减少应用程序关键部分的副作用。
这种方法的好处
- 非阻塞:后台任务确保 API 保持响应。
- 关注点分离:更新逻辑、通知和电子邮件处理是独立的。
- 函数式思维:数据库更新函数比较纯粹,比较容易测试。
- 可扩展性:后台处理比同步执行具有更好的可扩展性。
- 代码可靠性:减少副作用使调试更容易。
下一步是什么?
如果您有兴趣深入了解后端开发中的函数式编程,请考虑:
- 在后台作业中实现重试和故障处理。
- 使用 Kafka 或 RabbitMQ 的事件驱动架构。
- 探索 JavaScript 中的函数式编程库,例如 Ramda 或 Lodash/fp。
正如约翰·卡马克 (John Carmack) 所说,“以函数式风格进行编程有很多好处。
你应该在方便的时候做这件事,而在不方便的时候你应该仔细考虑这个决定。”
我一直在研究一种超级方便的工具,叫做LiveAPI。
LiveAPI可帮助您在几分钟内记录所有后端 API
使用 LiveAPI,您可以快速生成交互式 API 文档,允许用户直接从浏览器执行 API。
如果您厌倦了手动为 API 创建文档,这个工具可能会让您的生活更轻松。
鏂囩珷鏉ユ簮锛�https://dev.to/lovestaco/implementing-an-api-with-background-tasks-a-pragmatic-approach-5fbd