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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Storing a response keyed by an Idempotency-Key lets clients safely retry POSTs without duplicating side effects.
- 2A short-lived NX lock in Redis prevents two concurrent copies of the same request from both executing.
- 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.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/idempotent-post-requests-in-nestjs-with-redis-explained-typescript-60fa/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.