Module P-7·25 min read

Redis with ioredis, cache-aside pattern, Nodemailer, file uploads to S3/R2, outbound HTTP with fetch, and idempotent webhook processing.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module P-7 — Connecting External Services and Caching

What this module covers: Production APIs don't live in isolation. They send email, upload files to object storage, call third-party APIs, and cache expensive queries to keep response times fast. This module covers Redis with ioredis and the cache-aside pattern, sending transactional email with Nodemailer, uploading files to S3-compatible object storage, making outbound HTTP requests with the native fetch API, and processing webhooks idempotently. Each section gives you production-ready patterns, not just the happy path.


Redis and Caching with ioredis

Redis is an in-memory data store. The two primary use cases in a Node.js API are caching (store the result of an expensive query, serve it for subsequent requests) and session/token storage (store refresh tokens so they can be revoked).

bash

Redis client singleton

typescript

The Cache-Aside Pattern

The most common caching strategy: check the cache first, hit the database only on a miss, then populate the cache for next time.

typescript

A cold cache under a traffic spike is like one ATM with a thousand people reading the same "temporarily out of service" sign at once and independently deciding to walk to the same bank branch — every one of those thousand requests treats the miss as their own personal errand.

Cache Stampede: When a Miss Multiplies Itself

The getPostById function above has a race: if post:42 expires (or was never cached) and a thousand requests for it land within the same few milliseconds, every single one sees a cache miss and every single one queries the database — for the same row, at the same time. A popular post going viral is exactly when this hurts most: the cache is supposed to protect the database from load, and the moment it's most needed is the moment it stops helping.

Three mitigations, cheapest first:

Jittered TTLs — don't let a batch of keys expire at the exact same instant (common when they were all populated by the same batch job or warm-up script):

typescript

Locking — the first request to see a miss acquires a short-lived lock and populates the cache; everyone else waits briefly and retries the cache read instead of all hitting the database at once:

typescript

Probabilistic early refresh — recompute the value slightly before it actually expires, with a probability that rises as the remaining TTL shrinks, so a background refresh usually wins the race instead of the whole crowd hitting an actual expiry at once. Best suited to expensive-to-recompute values (a report, an aggregation) rather than a single-row lookup.

Pick locking for hot single keys under a thundering herd, jittered TTLs as a cheap default for anything cached in batches, and probabilistic early refresh for expensive-to-recompute values where a few extra seconds of staleness beats a cold recompute.

Cache invalidation

The cache must be invalidated when data changes — stale cache is worse than no cache:

typescript

Caching list queries

Lists are harder — when a post is created, which lists are stale? The simplest approach: use short TTLs for lists and longer TTLs for individual records.

typescript

Redis for refresh token storage

Using Redis instead of a Postgres table for refresh tokens — faster lookups, automatic expiry:

typescript

Sending Email with Nodemailer

Nodemailer is the standard Node.js library for sending email. In production you connect it to a transactional email provider (SendGrid, Resend, Postmark, SES) — never your own SMTP server.

bash

Email transport singleton

typescript

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.