Stalier is a stale-while-revalidate middleware for your backend
Stalier is cache strategy middleware controled by your frontend by using x-stalier-cache-control
header.
This means that instead of your backend sending Cache-Control
header to your browser for your browser to cache the returned data, your frontend will send the header X-Stalier-Cache-Control
to your backend for it to cache the returned data from your source of truth.
- It is an advanced middleware with support for stale-while-revalidate strategy.
- Stalier will act as a proxy that caches the response if it sees a
X-Stalier-Cache-Control
header. - Since it's embedded in your backend, it's much more efficient than using a separate proxy.
- It implements part of RFC7234 and RFC5861 but on the backend. It does not use
cache-control
since the cache is controlled by the frontend. - If you want both your browser and backend to cache the responses, you can use
x-stalier-cache-control
for requests andcache-control
for responses at the same time.
Table of contents
npm install stalier
or
yarn add stalier
Stalier do not provide a cache on it's own, nor does it provide a logger. But you provide them both in the constructor.
import express from 'express';
import cacheManager from 'cache-manager';
import redisStore from 'cache-manager-ioredis';
import { stalier } from 'stalier';
const redisCache = cacheManager.caching({ store: redisStore });
// Create a new express app
const app = express();
// add stalier middleware
app.use(stalier({ appName: 'test', cacheClient: redisCache }));
type StalierMiddlewareOptions = {
/**
* name of the upstream application
*/
appName: string;
/**
* client to use for caching
* should have an async `get` method and `set` method
*/
cacheClient: CacheClient;
/**
* function to generate a cache key per request
* Use a custom one to handle per user caching
* @default `<appName>-<HTTP Verb>-<path>`
*/
cacheKeyGen?: (req: Request) => string;
/**
* logger to use for logging
* should have a log, warn and error method that takes a message parameter
* @default `console`
*/
logger?: Logger;
};
import express from 'express';
import cacheManager from 'cache-manager';
import redisStore from 'cache-manager-ioredis';
import jsonwebtoken from 'jsonwebtoken';
import { stalier } from 'stalier';
// your user middleware
import { userMiddleware } from './userMiddleware';
const redisCache = cacheManager.caching({ store: redisStore });
const appName = 'test';
// cache key per user request that extracts the user email from your user middleware
const cacheKeyGen = (req: Request) => {
if(req.user) {
return `${appName}-${req.method}-${req.path}-${req.user.id}`;
}
return `${appName}-${req.method}-${req.path}`;
}
app.use(userMiddleware);
app.use(stalier({
appName,
cacheClient: redisCache,
cacheKeyGen,
}));
Stalier uses the NestJsInterceptor
to intercept the request and response, and uses the Cache Module to cache the response.
Stalier module allows you to instanciate a Cache with the cache-manager
library. The cacheOptions can take a single option or an array of options to use multicache strategy from cache-manager
.
Stalier module as two ways of instanciating a cache:
forRoot
- with options known at compile timeforRootAsync
- with options known at runtime, usually loaded from a config service
As the name suggests, your should only load Stalier module once in your application.
import { Controller, Get } from '@nestjs/common';
import { UseStalierInterceptor, StalierModule, UseCacheKeyGen } from 'stalier';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
StalierModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
appName: 'test',
cacheOptions: configService.get('CACHE_OPTIONS'),
isGlobal: true,
}),
inject: [ConfigService],
}),
],
controllers: [MyAppController],
})
interface StalierModuleOptions {
/**
* name of the app - used to generate cache key
*/
appName: string;
/**
* function to generate cache key from a request
*/
cacheKeyGen?: KeyGenFn;
/**
* options for cache-manager
*/
cacheOptions: StalierCacheManagerOptions | StalierCacheManagerOptions[];
/**
* if true, stalier cache will be global and shared across all modules
*/
isGlobal?: boolean;
}
interface StalierCacheManagerOptions {
/**
* cache-manager store to use
*/
store: 'memory' | 'none' | CacheStore | CacheStoreFactory;
/**
* maximum number of items to store in the cache - only for memory cache
*/
max?: number;
/**
* time to live in seconds - if not set no ttl is set by default
*/
ttl?: number;
}
You can load StalierInterceptor Globally (see useGlobalInterceptors) or per Controller (see useInterceptors).
@UseStalierInterceptor()
@Controller()
class MyAppController {
@Get('/default-key')
getDefaultKey() {
return { hello: 'world' };
}
// per user caching
@CacheKeyUser((req) => req.user.id)
@Get('/custom-key')
getCustomKey() {
return { hello: 'world' };
}
}
By default, stalier interceptor will use the request path as the cache key.
To handle per user caching, you can use the CacheKeyUser
decorator. You can apply it per controller or per method.
To apply a static key, use the CacheKey
decorator. Or to have fine grained control, you can use the CacheKeyGen
decorator.
Stalier is using the x-stalier-cache-control
header to control the cache behaviour within your frontend.
it supports the following params:
- s-maxage: time in seconds indicating the maximum time the response should be cached. A value of 0 means no caching.
- stale-while-revalidate: time in seconds indicating the time the response should be cached while revalidating. A value of 0 means no window for revalidation and only use cached content.
If you want a content to be cached for 10 seconds and have a revalidation window of 50 seconds, you can use the following headers:
GET /content HTTP/1.1
x-stalier-cache-control: s-maxage=10, stale-while-revalidate=50
if it's not present, it will be cached for 10 seconds, and get back the fresh content with the following headers:
HTTP/1.1 200 OK
x-cache-status: MISS
Requesting another time the same content within 10 seconds will return the cached content with the following headers:
HTTP/1.1 200 OK
x-cache-status: HIT
Requesting another time the same content within 50 seconds will return the cached content and try to refresh the cache in the background and return following headers:
HTTP/1.1 200 OK
x-cache-status: STALE
Another call will then return the refreshed content with the following headers:
HTTP/1.1 200 OK
x-cache-status: HIT