Rp0

Tidak ada produk di keranjang.

BELANJA PRODUK DIGITAL

admin@boimeningkat.com

+62 85276661500

Rp0

Tidak ada produk di keranjang.

Redis Adalah: Panduan Lengkap Caching Node.js (Express) Terintegrasi ETag, Rate Limit, Session Store, dan SWR

ARTIKEL POPULER

- Advertisement -

Redis Adalah: Panduan Pemula + Caching Node.js (Express) Terintegrasi (ETag, Rate Limit, Session, SWR)

Redis adalah open-source in-memory data structure store yang digunakan sebagai database, cache, dan message broker. Karena bekerja di memori, Redis mampu memberikan akses data sangat cepat dan sering dipakai untuk meningkatkan performa aplikasi Node.js, khususnya pada kebutuhan caching, session store, dan rate limiting.

Di artikel ini Anda akan mendapatkan versi lengkap dan terintegrasi (bukan potongan terpisah) tentang:

  • Apa itu Redis dan kenapa populer di Node.js
  • Caching API response dengan Redis
  • Caching per-route dengan ETag + conditional request (304 Not Modified)
  • Rate limiting dengan Redis (per IP + per route)
  • Session store Express menggunakan Redis (connect-redis)
  • Pola stale-while-revalidate untuk mencegah cache stampede
  • Kode final: 1 file server.js siap jalan

Apa Itu Redis?

Redis Adalah: Panduan Lengkap Caching Node.js (Express) Terintegrasi ETag, Rate Limit, Session Store, dan SWR
Redis

Redis adalah sistem penyimpanan data yang beroperasi di memori (RAM). Redis menyimpan data dalam format key-value dan mendukung berbagai struktur data seperti:

  • String
  • List
  • Hash
  • Set
  • Sorted Set

Redis sangat cocok untuk skenario yang membutuhkan akses data super cepat, misalnya:

  • caching respons API
  • session store login
  • leaderboard game
  • sistem antrian (queue)
  • rate limiting
  • analitik waktu nyata

Kenapa Redis Cocok untuk Caching?

Caching adalah proses menyimpan data yang sering diminta agar permintaan berikutnya tidak perlu menghitung ulang atau memanggil API/database lagi.

Redis cocok untuk caching karena:

  • operasi GET/SET cepat (in-memory)
  • punya TTL bawaan (expired otomatis)
  • mudah dipakai untuk key prefix (misalnya jobs:*)
  • mendukung pola anti stampede seperti SWR

Alur Caching yang Akan Kita Bangun

Untuk endpoint GET /jobs?search=node:

  1. Buat key cache: jobs:node
  2. Cek Redis:
    • ada → HIT → balas dari cache
    • tidak ada → MISS → fetch API eksternal → simpan ke Redis
  3. Hitung dan kirim ETag
  4. Jika client kirim If-None-Match, server bisa balas 304 Not Modified
  5. Terapkan rate limit agar endpoint tidak diserang
  6. Terapkan session store agar scalable
  7. Terapkan stale-while-revalidate agar tidak terjadi stampede
READ  Metode SDLC dalam Pengembangan Software: Waterfall, Prototype, Agile, dan Fountain Beserta Kelebihannya

Instal Redis (Cara Cepat)

Opsi Docker (paling cepat)

docker run -d --name redis -p 6379:6379 redis:7-alpine

Cek:

redis-cli ping
# PONG

Setup Project Node.js

Redis Adalah: Panduan Lengkap Caching Node.js (Express) Terintegrasi ETag, Rate Limit, Session Store, dan SWR
Ilustrasi Logo NodeJs

Buat folder dan install dependensi:

mkdir redis-node && cd redis-node
npm init -y
npm i express axios redis etag express-session connect-redis

KODE FINAL TERINTEGRASI (1 FILE) — server.js

Kode berikut sudah berisi:

✅ Redis cache
✅ ETag + 304
✅ rate limiting Redis
✅ session store Redis
✅ stale-while-revalidate + lock (anti stampede)
✅ fallback jika Redis down (server tetap jalan)

Buat file server.js:

'use strict';

const express = require('express');
const axios = require('axios');
const etag = require('etag');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const app = express();

// =========================
// Konfigurasi ENV
// =========================
const PORT = process.env.PORT || 3000;

const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
const SESSION_SECRET = process.env.SESSION_SECRET || 'ganti-secret-anda';

const HTTP_TIMEOUT_MS = Number(process.env.HTTP_TIMEOUT_MS || 8000);

// Cache SWR
const SOFT_TTL_SECONDS = Number(process.env.SOFT_TTL_SECONDS || 300);   // fresh 5 menit
const HARD_TTL_SECONDS = Number(process.env.HARD_TTL_SECONDS || 1800);  // stale allowed 30 menit

// Rate limit
const RATE_LIMIT = Number(process.env.RATE_LIMIT || 60); // 60 req
const RATE_WINDOW_SECONDS = Number(process.env.RATE_WINDOW_SECONDS || 60); // per 60 detik

// =========================
// Redis Client (node-redis v4)
// =========================
const redis = createClient({ url: REDIS_URL });

redis.on('error', (err) => {
  console.error('[Redis error]', err?.message || err);
});

async function initRedis() {
  if (!redis.isOpen) {
    await redis.connect();
    console.log('[Redis] connected');
  }
}

// =========================
// Express Session (Redis store)
// =========================
app.set('trust proxy', 1); // penting jika deploy di reverse proxy (Nginx, Cloudflare, dsb)

app.use(
  session({
    store: new RedisStore({
      client: redis,
      prefix: 'sess:',
    }),
    secret: SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      // secure: true, // aktifkan jika sudah HTTPS
      maxAge: 1000 * 60 * 60 * 2, // 2 jam
    },
  })
);

// =========================
// Helper Key Builder
// =========================
function normalizeSearch(search) {
  return (search || '').trim().toLowerCase();
}

function buildJobsCacheKey(search) {
  return `jobs:${normalizeSearch(search) || 'all'}`;
}

// =========================
// Rate Limit Middleware Redis
// =========================
function rateLimitRedis({ limit, windowSeconds, keyPrefix = 'rl' }) {
  return async function (req, res, next) {
    if (!redis.isOpen) return next();

    try {
      const ip = (req.headers['x-forwarded-for']?.split(',')[0] || req.ip || 'unknown').trim();
      const route = req.baseUrl + req.path;
      const windowKey = Math.floor(Date.now() / (windowSeconds * 1000));
      const key = `${keyPrefix}:${ip}:${route}:${windowKey}`;

      const count = await redis.incr(key);
      if (count === 1) {
        await redis.expire(key, windowSeconds);
      }

      res.set('X-RateLimit-Limit', String(limit));
      res.set('X-RateLimit-Remaining', String(Math.max(0, limit - count)));

      if (count > limit) {
        return res.status(429).json({
          error: 'Too Many Requests',
          message: 'Rate limit exceeded. Please try again later.',
        });
      }

      return next();
    } catch (err) {
      console.error('[rateLimitRedis]', err?.message || err);
      return next();
    }
  };
}

// =========================
// Fetch Upstream API
// =========================
// NOTE: GitHub Jobs API sudah deprecated.
// Kita pakai Remotive API yang masih aktif.
async function fetchJobsFromRemotive(search) {
  const url = 'https://remotive.com/api/remote-jobs';

  const resp = await axios.get(url, {
    timeout: HTTP_TIMEOUT_MS,
    params: search ? { search } : {},
    headers: { Accept: 'application/json' },
  });

  return Array.isArray(resp.data?.jobs) ? resp.data.jobs : [];
}

// =========================
// Stale-While-Revalidate (SWR)
// =========================
async function setSWR({ key, metaKey, softTtlSeconds, hardTtlSeconds, payload }) {
  const now = Date.now();
  const meta = {
    freshUntil: now + softTtlSeconds * 1000,
    createdAt: now,
  };

  await redis.setEx(key, hardTtlSeconds, payload);
  await redis.setEx(metaKey, hardTtlSeconds, JSON.stringify(meta));
}

async function triggerRevalidate({ key, metaKey, lockKey, softTtlSeconds, hardTtlSeconds, fetcher }) {
  if (!redis.isOpen) return;

  // lock 15 detik agar hanya 1 worker refresh
  const acquired = await redis.set(lockKey, '1', { NX: true, EX: 15 });
  if (!acquired) return;

  try {
    const newPayload = await fetcher();
    await setSWR({ key, metaKey, softTtlSeconds, hardTtlSeconds, payload: newPayload });
  } catch (err) {
    console.error('[SWR revalidate]', err?.message || err);
  } finally {
    await redis.del(lockKey).catch(() => {});
  }
}

async function getSWR({ key, softTtlSeconds, hardTtlSeconds, fetcher }) {
  const metaKey = `${key}:meta`;
  const lockKey = `${key}:lock`;

  if (!redis.isOpen) {
    // Redis down -> fallback langsung fetch
    const payload = await fetcher();
    return { payload, state: 'miss' };
  }

  const [payload, metaRaw] = await redis.mGet([key, metaKey]);

  // Jika cache ada
  if (payload && metaRaw) {
    const meta = JSON.parse(metaRaw);
    const now = Date.now();

    // Masih fresh
    if (now <= meta.freshUntil) {
      return { payload, state: 'fresh' };
    }

    // Stale -> balas payload cepat, refresh background
    triggerRevalidate({ key, metaKey, lockKey, softTtlSeconds, hardTtlSeconds, fetcher }).catch(() => {});
    return { payload, state: 'stale' };
  }

  // Cache miss total -> fetch sekarang
  const newPayload = await fetcher();
  await setSWR({ key, metaKey, softTtlSeconds, hardTtlSeconds, payload: newPayload });
  return { payload: newPayload, state: 'miss' };
}

// =========================
// Routes
// =========================

// Health check
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    redis: redis.isOpen ? 'connected' : 'disconnected',
    session: req.sessionID || null,
    time: new Date().toISOString(),
  });
});

// Demo session
app.get('/login-mock', (req, res) => {
  req.session.userId = 'user_123';
  res.json({ ok: true, sessionId: req.sessionID, userId: req.session.userId });
});

app.get('/me', (req, res) => {
  res.json({ userId: req.session.userId || null });
});

// Endpoint utama: /jobs
app.get(
  '/jobs',
  rateLimitRedis({ limit: RATE_LIMIT, windowSeconds: RATE_WINDOW_SECONDS }),
  async (req, res) => {
    const search = req.query.search ? String(req.query.search) : '';
    const key = buildJobsCacheKey(search);

    try {
      const { payload, state } = await getSWR({
        key,
        softTtlSeconds: SOFT_TTL_SECONDS,
        hardTtlSeconds: HARD_TTL_SECONDS,
        fetcher: async () => {
          const jobs = await fetchJobsFromRemotive(search);

          return JSON.stringify({
            source: 'remotive',
            query: search || null,
            count: jobs.length,
            jobs,
            generatedAt: new Date().toISOString(),
          });
        },
      });

      // ETag untuk conditional request
      const responseEtag = etag(payload);
      res.set('ETag', responseEtag);

      // Jika client kirim If-None-Match -> 304
      const ifNoneMatch = req.headers['if-none-match'];
      if (ifNoneMatch && ifNoneMatch === responseEtag) {
        res.set('X-Cache', state.toUpperCase());
        return res.status(304).end();
      }

      res.set('X-Cache', state.toUpperCase()); // FRESH / STALE / MISS
      res.set('Content-Type', 'application/json; charset=utf-8');
      return res.status(200).send(payload);
    } catch (err) {
      const message = err?.response?.data || err?.message || 'Unknown error';
      return res.status(502).json({ error: 'Upstream API error', message });
    }
  }
);

// =========================
// Start Server
// =========================
(async () => {
  try {
    await initRedis();
  } catch (e) {
    console.error('[Redis] failed to connect, continuing without cache:', e?.message || e);
  }

  app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
    console.log(`Try: http://localhost:${PORT}/jobs?search=node`);
  });
})();

Cara Menjalankan

node server.js

Uji:

  1. Request pertama (MISS)
curl -i "http://localhost:3000/jobs?search=node"
  1. Request kedua (FRESH/HIT)
curl -i "http://localhost:3000/jobs?search=node"
  1. Uji conditional request (304)
  • Ambil ETag dari response pertama
  • Jalankan:
curl -i "http://localhost:3000/jobs?search=node" -H 'If-None-Match: "<ETAG>"'

Penjelasan Ringkas Fitur Terintegrasi

Redis Adalah: Panduan Lengkap Caching Node.js (Express) Terintegrasi ETag, Rate Limit, Session Store, dan SWR
Gambar Penjelasan Ringkas ETag Caching

1) ETag + Conditional Request

ETag membuat browser/proxy tidak perlu download ulang payload jika sama. Server cukup balas 304.

READ  Cara Pindahan Hosting Antar cPanel Lengkap Gambar

2) Rate Limiting Redis

Mencegah abuse endpoint. Cocok untuk API publik, scraping, atau serangan request spam.

3) Session Store Redis

Session tidak disimpan di memory server, sehingga aplikasi tetap aman saat:

  • restart
  • multi server (load balancer)
  • scaling horizontal

4) Stale-While-Revalidate

Mencegah cache stampede:

  • request tetap cepat meski cache stale
  • hanya 1 request melakukan refresh (lock)

Kesimpulan

Redis adalah teknologi penting dalam sistem backend modern, terutama untuk Node.js. Dengan Redis Anda bisa membangun sistem caching yang cepat, scalable, dan stabil. Versi integrasi yang benar bukan hanya GET/SET biasa, tetapi juga perlu fitur tambahan seperti ETag, rate limiting, session store, dan stale-while-revalidate agar aplikasi siap untuk trafik nyata.

TINGGALKAN KOMENTAR

Silakan masukkan komentar anda!
Silakan masukkan nama Anda di sini

- Advertisement -

ARTIKEL TERBARU