Production-ready NestJS caching module with pluggable store adapters, a cache-aside service, and method-level
@Cacheable/@CacheEvictdecorators.
npm install @ciscode/cachekitInstall the peers that match what your app already uses:
# Always required
npm install @nestjs/common @nestjs/core
# Required when using the Redis store
npm install ioredisimport { Module } from "@nestjs/common";
import { CacheModule } from "@ciscode/cachekit";
@Module({
imports: [
CacheModule.register({
store: "memory",
ttl: 60, // default TTL in seconds (optional)
}),
],
})
export class AppModule {}import { Module } from "@nestjs/common";
import { CacheModule } from "@ciscode/cachekit";
@Module({
imports: [
CacheModule.register({
store: "redis",
ttl: 300,
redis: {
client: "redis://localhost:6379",
keyPrefix: "myapp:",
},
}),
],
})
export class AppModule {}import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { CacheModule } from "@ciscode/cachekit";
@Module({
imports: [
ConfigModule.forRoot(),
CacheModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
store: cfg.get<"redis" | "memory">("CACHE_STORE", "memory"),
ttl: cfg.get<number>("CACHE_TTL", 60),
redis: {
client: cfg.get<string>("REDIS_URL", "redis://localhost:6379"),
keyPrefix: cfg.get<string>("CACHE_PREFIX", "app:"),
},
}),
}),
],
})
export class AppModule {}Inject CacheService wherever you need direct cache access:
import { Injectable } from "@nestjs/common";
import { CacheService } from "@ciscode/cachekit";
@Injectable()
export class ProductsService {
constructor(private readonly cache: CacheService) {}
async getProduct(id: string) {
// Manual cache-aside pattern
const cached = await this.cache.get<Product>(`product:${id}`);
if (cached) return cached;
const product = await this.db.findProduct(id);
await this.cache.set(`product:${id}`, product, 120); // TTL = 120 s
return product;
}
async deleteProduct(id: string) {
await this.db.deleteProduct(id);
await this.cache.delete(`product:${id}`);
}
// wrap() β cache-aside in one call
async getAll(): Promise<Product[]> {
return this.cache.wrap(
"products:all",
() => this.db.findAllProducts(),
300, // TTL = 300 s
);
}
}| Method | Signature | Description |
|---|---|---|
get |
get<T>(key): Promise<T | null> |
Retrieve a value; returns null on miss or expiry |
set |
set<T>(key, value, ttl?): Promise<void> |
Store a value; ttl overrides module default |
delete |
delete(key): Promise<void> |
Remove a single entry |
clear |
clear(): Promise<void> |
Remove all entries (scoped to key prefix for Redis) |
has |
has(key): Promise<boolean> |
Return true if key exists and has not expired |
wrap |
wrap<T>(key, fn, ttl?): Promise<T> |
Return cached value or call fn, cache result, return it |
Cache the return value of a method automatically (cache-aside). The decorated method is only called on a cache miss; subsequent calls return the stored value.
Key templates β use {0}, {1}, β¦ to interpolate method arguments:
import { Injectable } from "@nestjs/common";
import { Cacheable } from "@ciscode/cachekit";
@Injectable()
export class UserService {
// Static key β same result cached for all calls
@Cacheable("users:all", 300)
async findAll(): Promise<User[]> {
return this.db.findAllUsers();
}
// Dynamic key β "user:42" for userId = 42
@Cacheable("user:{0}", 120)
async findById(userId: number): Promise<User> {
return this.db.findUser(userId);
}
// Multi-argument key β "org:5:user:99"
@Cacheable("org:{0}:user:{1}", 60)
async findByOrg(orgId: number, userId: number): Promise<User> {
return this.db.findUserInOrg(orgId, userId);
}
}Evict (delete) a cache entry after the decorated method completes successfully. If the method throws, the entry is not evicted.
import { Injectable } from "@nestjs/common";
import { CacheEvict } from "@ciscode/cachekit";
@Injectable()
export class UserService {
// Evict "users:all" whenever a user is created
@CacheEvict("users:all")
async createUser(dto: CreateUserDto): Promise<User> {
return this.db.createUser(dto);
}
// Evict the specific user entry β "user:42" for userId = 42
@CacheEvict("user:{0}")
async updateUser(userId: number, dto: UpdateUserDto): Promise<User> {
return this.db.updateUser(userId, dto);
}
// Evict on delete
@CacheEvict("user:{0}")
async deleteUser(userId: number): Promise<void> {
await this.db.deleteUser(userId);
}
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
store |
"memory" | "redis" |
β | β | Backing store adapter |
ttl |
number |
β | undefined |
Default TTL in seconds for all set() calls |
redis |
RedisCacheStoreOptions |
When store: "redis" |
β | Redis connection config |
| Field | Type | Required | Description |
|---|---|---|---|
client |
string | Redis |
β | Redis URL (redis://β¦) or existing ioredis instance |
keyPrefix |
string |
β | Prefix for all keys, e.g. "myapp:" |
src/
βββ index.ts # Public API exports
βββ cache-kit.module.ts # CacheModule (dynamic NestJS module)
βββ constants.ts # DI tokens: CACHE_STORE, CACHE_MODULE_OPTIONS
β
βββ ports/
β βββ cache-store.port.ts # ICacheStore interface
β
βββ adapters/
β βββ in-memory-cache-store.adapter.ts # Map-backed adapter (no deps)
β βββ redis-cache-store.adapter.ts # ioredis-backed adapter
β
βββ services/
β βββ cache.service.ts # CacheService (public API)
β
βββ decorators/
β βββ cacheable.decorator.ts # @Cacheable
β βββ cache-evict.decorator.ts # @CacheEvict
β
βββ utils/
βββ cache-service-ref.ts # Singleton holder for decorators
βββ resolve-cache-key.util.ts # {0}, {1} key template resolver
- Never pass credentials directly in source code β use environment variables or
ConfigService - The Redis
keyPrefixisolates cache entries from other apps sharing the same instance clear()without a key prefix willFLUSHDBthe entire Redis database β use prefixes in production
MIT Β© CisCode
// src/dto/create-example.dto.ts
import { IsString, IsNotEmpty } from "class-validator";
export class CreateExampleDto {
@IsString()
@IsNotEmpty()
name: string;
}// src/index.ts
export { ExampleKitModule } from "./example-kit.module";
export { ExampleService } from "./services/example.service";
export { CreateExampleDto } from "./dto/create-example.dto";# Development
npm run build # Build the package
npm run build:watch # Build in watch mode
npm run typecheck # TypeScript type checking
# Testing
npm test # Run tests
npm run test:watch # Run tests in watch mode
npm run test:cov # Run tests with coverage
# Code Quality
npm run lint # Run ESLint
npm run format # Check formatting
npm run format:write # Fix formatting
# Release
npx changeset # Create a changeset
npm run release # Publish to npm (CI does this)This template uses Changesets for version management.
git checkout develop
git checkout -b feature/my-feature
# Make your changesnpx changesetSelect the change type:
- patch - Bug fixes
- minor - New features (backwards compatible)
- major - Breaking changes
git add .
git commit -m "feat: add new feature"
git push origin feature/my-feature
# Create PR β develop- Automation opens "Version Packages" PR
- Merge to
masterto publish
Tests are MANDATORY for all public APIs.
// src/services/example.service.spec.ts
describe("ExampleService", () => {
let service: ExampleService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [ExampleService],
}).compile();
service = module.get(ExampleService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
it("should process data correctly", async () => {
const result = await service.doSomething("test");
expect(result).toBe("Processed: test");
});
});Coverage threshold: 80%
Configured in tsconfig.json:
import { ExampleService } from "@services/example.service";
import { CreateExampleDto } from "@dtos/create-example.dto";
import { Example } from "@entities/example.entity";
import { ExampleRepository } from "@repos/example.repository";Available aliases:
@/*βsrc/*@controllers/*βsrc/controllers/*@services/*βsrc/services/*@entities/*βsrc/entities/*@repos/*βsrc/repositories/*@dtos/*βsrc/dto/*@guards/*βsrc/guards/*@decorators/*βsrc/decorators/*@config/*βsrc/config/*@utils/*βsrc/utils/*
- β Input validation on all DTOs (class-validator)
- β Environment variables for secrets
- β No hardcoded credentials
- β Proper error handling
- β Rate limiting on public endpoints
This template includes comprehensive Copilot instructions in .github/copilot-instructions.md:
- Module architecture guidelines
- Naming conventions
- Testing requirements
- Documentation standards
- Export patterns
- Security best practices
- Architecture - Detailed architecture overview
- Release Process - How to release versions
- Copilot Instructions - AI development guidelines
- Rename the module: Update
package.jsonname - Update description: Modify
package.jsondescription - Configure exports: Edit
src/index.ts - Add dependencies: Update
peerDependenciesanddependencies - Customize structure: Add/remove directories as needed
β DO export:
- Module
- Services
- DTOs
- Guards
- Decorators
- Types/Interfaces
β DON'T export:
- Entities
- Repositories
Entities and repositories are internal implementation details.
- MAJOR (x.0.0) - Breaking changes
- MINOR (0.x.0) - New features (backwards compatible)
- PATCH (0.0.x) - Bug fixes
- All tests passing (80%+ coverage)
- No ESLint warnings
- TypeScript strict mode passing
- All public APIs documented (JSDoc)
- README updated
- Changeset created
- Breaking changes documented
-
.env.exampleupdated (if needed)
MIT
See CONTRIBUTING.md
Made with β€οΈ by CisCode