Memcached module and service for Nest,
a progressive Node.js framework for building efficient and scalable server-side applications.
npm install @andreafspeziale/nestjs-memcached
yarn add @andreafspeziale/nestjs-memcached
pnpm add @andreafspeziale/nestjs-memcached
The module is Global by default.
src/core/core.module.ts
import { Module } from '@nestjs/common';
import { MemcachedModule } from '@andreafspeziale/nestjs-memcached';
@Module({
imports: [
MemcachedModule.forRoot({
connections: [
{
host: 'localhost',
port: 11211,
},
],
ttl: 60,
ttr: 30,
superjson: true,
keyProcessor: (key) => `prefix_${key}`,
wrapperProcessor: ({ value, ttl, ttr }) => ({
content: value,
ttl,
...(ttr ? { ttr } : {}),
createdAt: new Date(),
}),
log: true,
}),
],
....
})
export class CoreModule {}
- For localhost single connection you can omit the
connections
property. Alternativelyconnections: { locations: [{ host: 'localhost', port: 11211 }], options: { poolSize: 20 } }
or simplyconnections: { options: { poolSize: 20 } }
- For multiple connections you can omit the
port
property if the server is using the default one ttl
is the global time to livettr
is the global optional time to refresh- Typically when caching a JS object like
Date
you will get back astring
from the cache, superjson willstringify
on cachesets
adding metadata in order to laterparse
on cachegets
and retrieve the initial "raw" data keyProcessor
is the global optional key processor function which process your cache keyswrapperProcessor
is the global optional wrapper processor function which wraps the value to be cached and adds metadatalog
enable or disable logging (LoggerService
must be provided, check Extra Providers section)
src/core/core.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MemcachedModule } from '@andreafspeziale/nestjs-memcached';
import { Config } from './config';
@Module({
imports: [
ConfigModule.forRoot({
....
}),
MemcachedModule.forRootAsync({
useFactory: (cs: ConfigService<Config, true>) => cs.get<ConfigService['memcached']>('memcached'),
inject: [ConfigService],
}),
],
....
})
export class CoreModule {}
use the client and create your own service
src/samples/samples.service.ts
import { Injectable } from '@nestjs/common';
import {
InjectMemcachedOptions,
InjectMemcached,
MemcachedClient,
MemcachedModuleOptions,
MemcachedClient
} from '@andreafspeziale/nestjs-memcached';
@Injectable()
export class SamplesService {
constructor(
@InjectMemcachedOptions()
private readonly memcachedModuleOptions: MemcachedModuleOptions, // Showcase purposes
@InjectMemcached() private readonly memcachedClient: MemcachedClient
) {}
....
}
out of the box service with a set of features
src/samples/samples.facade.ts
import { MemcachedService } from '@andreafspeziale/nestjs-memcached';
import { SampleReturnType } from './samples.interfaces'
@Injectable()
export class SamplesFacade {
constructor(
private readonly memcachedService: MemcachedService,
) {}
async sampleMethod(): Promise<SampleReturnType> {
const cachedItem = await this.memcachedService.get<string>('key');
if(cachedItem === null) {
....
await this.memcachedService.set<string>('key', 'value');
....
}
}
}
You can also set all the proper Processors
and CachingOptions
inline in order to override the global values specified during the MemcachedModule
import
await this.memcachedService.set<string>('key', 'value', { ttl: 100 });
The exported MemcachedService
is an opinionated wrapper around memcached trying to be unopinionated as much as possibile at the same time.
setWithMeta
enables refresh-ahead
cache pattern in order to let you add a logical expiration called ttr (time to refresh)
to the cached data and more.
So each time you get some cached data it will contain additional properties in order to help you decide whatever business logic needs to be applied.
I usually expose an /healthz
controller from my microservices in order to check third parties connection.
src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
HealthCheckResult,
HealthIndicatorResult,
MicroserviceHealthIndicator,
} from '@nestjs/terminus';
import { Transport } from '@nestjs/microservices';
import { Config } from '../config';
import { ConfigService } from '@nestjs/config';
@Controller('healthz')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly microservice: MicroserviceHealthIndicator,
private readonly cs: ConfigService<Config, true>,
) {}
@Get()
check(): Promise<HealthCheckResult> {
return this.health.check([
(): Promise<HealthIndicatorResult> =>
this.microservice.pingCheck('memcached', {
transport: Transport.TCP,
options: {
host: this.cs.get<Config['memcached']>('memcached').connections?.[0].host,
port: this.cs.get<Config['memcached']>('memcached').connections?.[0].port,
},
}),
]);
}
}
As mentioned above I usually init my DynamicModules
injecting the ConfigService
exposed by the ConfigModule
(@nestjs/config
package). This is where I validate my environment variables using a schema validator of my choice, so far I tried joi
, class-validator
and zod
.
This is an example using joi
but you should tailor it based on your needs, starting by defining a Config
interface:
src/config/config.interfaces.ts
import {
BaseWrapper,
MemcachedConfig,
} from '@andreafspeziale/nestjs-memcached';
/**
* Cached data shape leveraging metadata feature
* {
* content: T;
* ttl: number;
* ttr?: number;
* version: number;
* created: Date;
* }
*/
export interface CachedMetaConfig {
version: number;
created: Date;
}
export type Cached<T = unknown> = BaseWrapper<T> & CachedMetaConfig;
export type Config = .... & MemcachedConfig<unknown, Cached>;
src/config/config.schema.ts
import {
MEMCACHED_HOST,
MEMCACHED_PORT,
MEMCACHED_TTL,
MEMCACHED_TTR,
MEMCACHED_VERSION
} from '@andreafspeziale/nestjs-memcached';
import {
MEMCACHED_PREFIX
} from './config.defaults';
const BASE_SCHEMA = ....;
const MEMCACHED_SCHEMA = Joi.object({
MEMCACHED_HOST: Joi.string().default(MEMCACHED_HOST),
MEMCACHED_PORT: Joi.number().default(MEMCACHED_PORT),
MEMCACHED_TTL: Joi.number().default(MEMCACHED_TTL),
MEMCACHED_TTR: Joi.number().less(Joi.ref('MEMCACHED_TTL')).default(MEMCACHED_TTR),
MEMCACHED_PREFIX: Joi.string().default(MEMCACHED_PREFIX),
MEMCACHED_VERSION: Joi.number().default(MEMCACHED_VERSION),
});
export const envSchema = Joi.object()
.concat(BASE_SCHEMA);
.concat(MEMCACHED_SCHEMA)
src/config/index.ts
import { Config } from './config.interfaces';
export * from './config.interfaces';
export * from './config.schema';
export default (): Config => ({
....,
memcached: {
connections: [
{
host: process.env.MEMCACHED_HOST,
port: parseInt(process.env.MEMCACHED_PORT, 10),
},
],
ttl: parseInt(process.env.MEMCACHED_TTL, 10),
...(process.env.MEMCACHED_TTR ? { ttr: parseInt(process.env.MEMCACHED_TTR, 10) } : {}),
wrapperProcessor: ({ value, ttl, ttr }) => ({
content: value,
ttl,
...(ttr ? { ttr } : {}),
version: parseInt(process.env.MEMCACHED_VERSION, 10),
created: new Date(),
}),
keyProcessor: (key: string) =>
`${process.env.MEMCACHED_PREFIX}::V${process.env.MEMCACHED_VERSION}::${key}`,
},
});
src/core/core.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MemcachedModule } from '@andreafspeziale/nestjs-memcached';
import config, { envSchema, Config } from '../config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [config],
validationSchema: envSchema,
}),
MemcachedModule.forRootAsync({
useFactory: (cs: ConfigService<Config, true>) => cs.get<Config['memcached']>('memcached'),
inject: [ConfigService],
}),
....
],
})
export class CoreModule {}
src/users/users.facade.ts
import { MemcachedService } from '@andreafspeziale/nestjs-memcached';
import { Cached } from '../config';
import { User } from './users.interfaces'
@Injectable()
export class UsersFacade {
constructor(
private readonly usersService: UsersService,
private readonly memcachedService: MemcachedService
) {}
async getUser(id: string): Promise<User> {
const cachedUser = await this.memcachedService.get<Cached<User>(
id, { superjson: true }
);
/**
* Cached data shape leveraging metadata feature
* {
* content: User;
* ttl: number;
* ttr?: number;
* version: number;
* created: Date;
* }
*/
if(cachedItem === null) {
....
await this.memcachedService.setWithMeta<User, Cached<User>>(
id, user, { superjson: true }
);
....
return user;
}
return cachedUser.content;
}
This is one of the most recent additions.
I was looking for a nice way to introduce logging after I created and published the nestjs-log
npm package.
My thoughts were:
nestjs-log
- can be declared as peerDependency and be installed as devDependency of consumer packages which may need logging
- can be installed as dependency of applications which will need logging for sure
nestjs-memcached
- can be a
nestjs-log
consumer - can be installed as dependency of applications (or even packages, why not?) which will need caching
- can be a
application
which can install both the above packages and orchestrate everything like configs and Providers DI
A visual representation can be
In terms of code, in nestjs-memcached
itself:
import { Injectable, Optional } from '@nestjs/common';
import { LoggerService } from '@andreafspeziale/nestjs-log';
import {
InjectMemcachedOptions,
InjectMemcached,
InjectMemcachedLogger,
} from './memcached.decorators';
....
@Injectable()
export class MemcachedService {
....
constructor(
@InjectMemcachedOptions()
private readonly memcachedModuleOptions: MemcachedModuleOptions,
@InjectMemcached() private readonly memcachedClient: MemcachedClient,
@Optional()
@InjectMemcachedLogger()
private readonly logger?: LoggerService,
) {
this.memcachedModuleOptions.log && this.logger?.setContext(MemcachedService.name);
....
}
}
In your application:
src/core/core.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import { LoggerModule, LoggerService } from '@andreafspeziale/nestjs-log';
import { getMemcachedLoggerToken, MemcachedModule } from '@andreafspeziale/nestjs-memcached';
import config, { envSchema, Config } from '../config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [config],
validationSchema: envSchema,
}),
LoggerModule.forRootAsync({
useFactory: (cs: ConfigService<Config, true>) => cs.get<Config['logger']>('logger'),
inject: [ConfigService],
}),
MemcachedModule.forRootAsync({
useFactory: (cs: ConfigService<Config, true>) => cs.get<Config['memcached']>('memcached'),
inject: [ConfigService],
extraProviders: [{ provide: getMemcachedLoggerToken(), useExisting: LoggerService }],
}),
....
],
})
export class CoreModule {}
The above extraProviders
option is optional and the actual logging is driven by:
- the application logger level configured within the
LoggerModule
- the
MemcachedModule
log
configuration key
docker compose -f compose-test.yaml up -d
pnpm test
- Author - Andrea Francesco Speziale
- Website - https://nestjs.com
- Twitter - @nestframework
nestjs-memcached MIT licensed.