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', {
to: user.email,
subject: 'Profile Updated',
body: 'Your profile has been successfully updated. If it was not done by you, please contact support.',
});
}
The Pragmatic Balance
Not everything can be purely functional—real-world applications need to interact with databases, file systems, and external services.
As Carmack notes, "Avoiding the worst in a broader context is generally more important than achieving perfection in limited cases."
Rather than enforcing strict purity everywhere, the goal is to minimize side effects in critical parts of our application while handling necessary mutations in a controlled manner.
Benefits of This Approach
- Non-blocking: Background tasks ensure the API remains responsive.
- Separation of concerns: The update logic, notifications, and email handling are independent.
- Functional mindset: The database update function is pure, making it easier to test.
- Scalability: Background processing scales better than synchronous execution.
- Code reliability: Reduced side effects make debugging easier.
What’s Next?
If you’re interested in diving deeper into functional programming in backend development, consider:
- Implementing retries and failure handling in background jobs.
- Using event-driven architectures with Kafka or RabbitMQ.
- Exploring functional programming libraries in JavaScript, like Ramda or Lodash/fp.
As John Carmack suggests, “Programming in a functional style provides benefits.
You should do it whenever it is convenient, and you should think hard about the decision when it isn’t convenient.”
I’ve been working on a super-convenient tool called LiveAPI.
LiveAPI helps you get all your backend APIs documented in a few minutes
With LiveAPI, you can quickly generate interactive API documentation that allows users to execute APIs directly from the browser.
If you’re tired of manually creating docs for your APIs, this tool might just make your life easier.
Top comments (0)