APIs are the backbone of modern applications, but sometimes they need to do more than just CRUD operations.
Consider a scenario where you need to update a user’s profile while also sending background notifications and emails—efficiently and without blocking the main request.
Let’s walk through how to achieve this using a structured, functional programming-inspired approach.
The Problem Statement
We need to implement an Update User API that:
- Accepts an update request for a username or phone number.
- Identifies the changed field.
- Updates the database accordingly.
- Fetches the user’s device token and sends a notification in the background.
- Sends an email to the user asynchronously.
- Returns an appropriate response to the client.
Why a Functional Approach?
John Carmack, a legendary game developer, argues that functional programming reduces side effects and improves software reliability.
In his view, many flaws in software development arise because programmers don’t fully understand all the possible states their code may execute in. This issue is magnified in multithreaded environments, where race conditions can lead to unpredictable behavior.
Functional programming mitigates these problems by making state explicit and reducing unintended side effects.
Even though we’re not using Haskell or Lisp, we can still apply functional programming principles in mainstream languages like JavaScript and TypeScript.
As Carmack puts it, "No matter what language you work in, programming in a functional style provides benefits."
By designing our API with pure functions, immutability, and clear separation of concerns, we improve testability, maintainability, and scalability.
Designing the API
1. Route Definition
We define an endpoint in our backend framework (e.g., Nest.js, Express, or Django):
PATCH /api/user/update
2. Request Payload
The request should include the fields that need to be updated:
{
// "userId": "12345",
"username": "newUser123",
"phoneNumber": "9876543210"
}
3. Implementation in Node.js (Nest.js Example)
We follow a structured approach to separate concerns and keep our functions testable and reusable.
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. Database Update Function (Pure Function Approach)
The following function updates the database and ensures data consistency:
async updateUser(userId: string, updates: Partial<User>) {
return this.userModel.findByIdAndUpdate(userId, updates, { new: true });
}
John Carmack emphasizes that pure functions only operate on their inputs and return computed values without modifying shared state. This approach ensures:
- Thread safety: No unintended side effects.
- Reusability: Easy to transplant into new environments.
- Testability: Always returns the same output for the same input.
5. Background Notification Handling
We fetch the user’s token and send a push notification without blocking the main request.
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. Sending Emails in the Background
Emails can be slow, so we offload them to a worker queue like BullMQ/ Kafka:
async sendUpdateEmail(user: User) {
emailQueue.add('sendEmail
