DEV Community

Cover image for Implementing an API with Background Tasks: A Pragmatic Approach
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

63 6 7 10 7

Implementing an API with Background Tasks: A Pragmatic Approach

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:

  1. Accepts an update request for a username or phone number.
  2. Identifies the changed field.
  3. Updates the database accordingly.
  4. Fetches the user’s device token and sends a notification in the background.
  5. Sends an email to the user asynchronously.
  6. 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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

2. Request Payload

The request should include the fields that need to be updated:

{
  // "userId": "12345",
  "username": "newUser123",
  "phoneNumber": "9876543210"
}
Enter fullscreen mode Exit fullscreen mode

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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.');
  }
}
Enter fullscreen mode Exit fullscreen mode

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.',
  });
}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.

Image description

If you’re tired of manually creating docs for your APIs, this tool might just make your life easier.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (7)

Collapse
 
oaymohsin profile image
Mohsin_Zaheer

Logics in Controller?

Collapse
 
lovestaco profile image
Athreya aka Maneshwar

No It would come in service itself, for the blog purpose did this

Collapse
 
nadeem_zia_257af7e986ffc6 profile image
nadeem zia

Interesting to read

Collapse
 
lovestaco profile image
Athreya aka Maneshwar

Thanks :)

Collapse
 
dfedotov profile image
Dmitry Fedotov

Route definition can be at least based on the REST principles

Collapse
 
lovestaco profile image
Athreya aka Maneshwar

Yupp

Collapse
 
shannon_ions_d9ea157cd09 profile image
Shannon I'Ons

Your db update function is not pure. The function connects to a database, which is a side effect.

SurveyJS custom survey software

JavaScript Form Builder UI Component

Generate dynamic JSON-driven forms directly in your JavaScript app (Angular, React, Vue.js, jQuery) with a fully customizable drag-and-drop form builder. Easily integrate with any backend system and retain full ownership over your data, with no user or form submission limits.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay