typescript 66 lines · 7 steps

Idempotent POST requests in NestJS with Redis

A NestJS interceptor uses an Idempotency-Key and Redis to make retried POST requests safe to replay.

Explained by highlit
1import {
2 CallHandler,
3 ConflictException,
4 ExecutionContext,
5 Injectable,
6 NestInterceptor,
7} from '@nestjs/common';
8import { Reflector } from '@nestjs/core';
9import { Request, Response } from 'express';
10import { Redis } from 'ioredis';
11import { Observable, of } from 'rxjs';
12import { tap } from 'rxjs/operators';
13 
14interface CachedResponse {
15 status: number;
16 body: unknown;
17}
18 
19@Injectable()
20export class IdempotencyInterceptor implements NestInterceptor {
21 private readonly ttlSeconds = 60 * 60 * 24;
22 
23 constructor(private readonly redis: Redis) {}
24 
25 async intercept(
26 context: ExecutionContext,
27 next: CallHandler,
28 ): Promise<Observable<unknown>> {
29 const http = context.switchToHttp();
30 const req = http.getRequest<Request>();
31 const res = http.getResponse<Response>();
32 
33 const key = req.header('Idempotency-Key');
34 if (req.method !== 'POST' || !key) {
35 return next.handle();
36 }
37 
38 const scopedKey = `idempotency:${req.route?.path ?? req.path}:${key}`;
39 const lockKey = `${scopedKey}:lock`;
40 
41 const cached = await this.redis.get(scopedKey);
42 if (cached) {
43 const { status, body } = JSON.parse(cached) as CachedResponse;
44 res.status(status);
45 return of(body);
46 }
47 
48 const acquired = await this.redis.set(lockKey, '1', 'EX', 30, 'NX');
49 if (!acquired) {
50 throw new ConflictException(
51 'A request with this Idempotency-Key is already being processed',
52 );
53 }
54 
55 return next.handle().pipe(
56 tap(async (body) => {
57 const payload: CachedResponse = { status: res.statusCode, body };
58 await this.redis
59 .multi()
60 .set(scopedKey, JSON.stringify(payload), 'EX', this.ttlSeconds)
61 .del(lockKey)
62 .exec();
63 }),
64 );
65 }
66}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Storing a response keyed by an Idempotency-Key lets clients safely retry POSTs without duplicating side effects.
  2. 2A short-lived NX lock in Redis prevents two concurrent copies of the same request from both executing.
  3. 3Piping into the response stream with tap lets you cache the result only after the handler succeeds.

Related explainers

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react';
 
const cache = new Map();
const inflight = new Map();

Building a stale-while-revalidate hook in React

caching request-deduplication custom-hooks
Advanced 10 steps
php
<?php
 
namespace App\Services;
 

Building a cached daily leaderboard in Laravel

caching aggregation eager-loading
Intermediate 9 steps

Share this explainer

Here's the card — post it anywhere.

Idempotent POST requests in NestJS with Redis — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code