Idempotency Explained: Ensuring Reliable API Calls. A practical example in Nestjs

João Paulo - Sep 14 - - Dev Community

Idempotency refers to an operation that can be performed multiple times without causing additional effects. In other words, no matter how many times the same action is repeated, the result will always be the same as if it had been executed only once.

Idempotency ensures that the operation is only processed once, even if it's executed multiple times, preventing duplicate payments or unintended outcomes. This is particularly useful in scenarios where operations, such as processing a credit transaction, might be retried due to network failures or system crashes.

In the context of web development, certain HTTP methods are idempotent and others no.

methods-idempotency

  • GET: Retrieving a resource using a GET request should not change the state of the resource on the server. Whether you request the same resource once or multiple times, the result should be the same.
  • PUT: Updating a resource using a PUT request should result in the same resource state regardless of how many times the request is made with the same data.

Some HTTP methods are not idempotent, meaning that repeated applications of the same request can lead to different results. Two common examples are POST and PATCH:

  • POST: The POST method is typically used to create new resources or submit data. Each POST request can create a new resource or initiate a new action, so sending the same POST request multiple times can result in multiple new resources or actions being created.
  • PATCH: The PATCH method is used to apply partial modifications to a resource. Since the PATCH request updates only certain fields, the outcome can change with each request, depending on the state of the resource before the update.

Why Idempotency Matters

Idempotency is crucial in systems where reliability and consistency are important. It helps in:

  1. Fault Tolerance: Idempotent operations ensure that retrying operations won’t lead to inconsistent states or unintended effects.
  2. Simplicity: Designing systems with idempotent operations can simplify error handling and recovery processes.
  3. Concurrency: Idempotency helps in managing concurrent operations, reducing the risk of conflicting changes.

Implementing an idempotent endpoint in Nestjs

1. Create the Idempotency Middleware

import {
  Injectable,
  NestMiddleware,
  BadRequestException,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { IdempotencyService } from '../services/idempotency.service';

@Injectable()
export class IdempotencyMiddleware implements NestMiddleware {
  constructor(private readonly idempotencyService: IdempotencyService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const idempotencyKey = req.headers['x-idempotency-key'] as string;
    if (!idempotencyKey) {
      throw new BadRequestException('Idempotency key is missing');
    }

    const cachedKey = await this.idempotencyService.getKey(idempotencyKey);

    if (cachedKey) {
      res.status(200).json(cachedKey);
      return;
    }

    res.on('finish', async () => {
      if (res.statusCode < 400) {
        await this.idempotencyService.saveKey(idempotencyKey);
      }
    });
    next();
  }
}

Enter fullscreen mode Exit fullscreen mode

2. Create the Idempotency Service

import { Inject, Injectable } from '@nestjs/common';
import {
  iDistributedCacheService,
  IDistributedCacheService,
} from './distributed-cache.service';
import { TEN_MINUTES } from '../constants/app.constants';

@Injectable()
export class IdempotencyService {
  constructor(
    @Inject(iDistributedCacheService)
    private readonly distributedCache: IDistributedCacheService,
  ) {}

  async getKey(key: string): Promise<string> {
    return await this.distributedCache.get(key);
  }

  async saveKey(key: string): Promise<void> {
    this.distributedCache.set(key, key, TEN_MINUTES);
  }
}

Enter fullscreen mode Exit fullscreen mode

Note, in this case i used a general cache service, you can store it in a simple HashMap local or something more robust like redis

3. Register the middleware in your app module

import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { TransactionsModule } from './modules/transactions/transactions.module';
import { PrismaModule } from './modules/prisma/prisma.module';
import { PrismaService } from './modules/prisma/prisma.service';
import { ClientsModule } from './modules/clients/clients.module';
import { ConfigModule } from '@nestjs/config';
import { IdempotencyMiddleware } from './libs/commons/middlewares/idempotency.middleware';
import { RedisDistributedCacheService } from './libs/commons/services/redis-distributed-cache.service';
import { iDistributedCacheService } from './libs/commons/services/distributed-cache.service';
import { IdempotencyService } from './libs/commons/services/idempotency.service';

@Module({
  imports: [
  ],
  providers: [
    IdempotencyService
  ],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(IdempotencyMiddleware)
      .forRoutes({ path: 'transactions', method: RequestMethod.POST });
  }
}

Enter fullscreen mode Exit fullscreen mode

Yow can now test the implemetation. Make a POST request to the route that you registered with the x-idempotency-key in header. The server should not proccess duplicate requests.

Thank you for reaching this point. If you need to contact me, here is my email: joaopauloj405@gmail.com

. . . . . .
Terabox Video Player