Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 70 additions & 17 deletions docs/integrations/nestjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@

TriCache provides an official NestJS dynamic module (`TriCacheModule`) and declarative method decorators (`@Cacheable`, `@CacheEvict`) for high-concurrency NestJS microservices.

### Ready-to-run NestJS 11 demo

A self-contained microservice lives at [`examples/nestjs-microservice`](https://github.com/Kareem411/TriCache/tree/main/examples/nestjs-microservice). It exercises `TriCacheModule.register()`, `@Cacheable({ ttl, tags })`, `@CacheEvict({ tags })`, and the exported `CACHE_MANAGER` / `TriCacheStore` cache-manager adapter.

```bash
pnpm install && pnpm build
cd examples/nestjs-microservice
pnpm install
pnpm dev
```

Then follow the curl walkthrough in that README (`GET /items/:id` hit vs `PATCH` eviction, plus `PUT`/`GET /store/notes/:id`).

---

## Installation & Module Registration

`TriCacheModule.register(options)` forwards `options` to `CacheService.create()` (`CacheOptions`). The dynamic module is always registered as `global: true` and exports `TRICACHE_SERVICE` (`CacheService`) plus `CACHE_MANAGER` (`TriCacheStore`). There is no `preset` or `isGlobal` field on this options object.

### Synchronous Registration

In your root `AppModule`:
Expand All @@ -19,10 +34,10 @@ import { TriCacheModule } from 'tricache/nestjs';
@Module({
imports: [
TriCacheModule.register({
preset: 'microservice',
redisHost: process.env.REDIS_HOST ?? '127.0.0.1',
namespace: 'orders-service',
redisHost: process.env.REDIS_HOST,
redisPort: Number(process.env.REDIS_PORT ?? 6379),
isGlobal: true, // Exports CacheService across all NestJS modules
disableRedis: !process.env.REDIS_HOST,
}),
],
})
Expand All @@ -44,10 +59,10 @@ import { TriCacheModule } from 'tricache/nestjs';
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
preset: 'microservice',
namespace: 'orders-service',
redisHost: config.get<string>('REDIS_HOST'),
redisPort: config.get<number>('REDIS_PORT'),
isGlobal: true,
redisPort: config.get<number>('REDIS_PORT') ?? 6379,
disableRedis: !config.get<string>('REDIS_HOST'),
}),
}),
],
Expand All @@ -63,17 +78,21 @@ export class AppModule {}
Automatically caches method return values with Singleflight coalescing and SWR:

```typescript
import { Injectable } from '@nestjs/common';
import { Cacheable } from 'tricache/nestjs';
import { Inject, Injectable } from '@nestjs/common';
import type { CacheService } from 'tricache';
import { Cacheable, TRICACHE_SERVICE } from 'tricache/nestjs';

@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
constructor(
@Inject(TRICACHE_SERVICE) readonly cacheService: CacheService,
private readonly prisma: PrismaService,
) {}

@Cacheable({
key: (userId: string) => `user:${userId}`,
ttlSec: 300,
swrSec: 60,
ttl: 300,
swr: 60,
tags: ['users'],
})
async getUserById(userId: string) {
Expand All @@ -82,15 +101,22 @@ export class UserService {
}
```

`ttl` is seconds by default (`ttlUnit?: 'seconds' | 'milliseconds'`). Decorators look up the engine on `this.cacheService`, `this.cache`, or `this.cacheStore.cache`.

### `@CacheEvict`
Evicts specific keys or tags upon mutation:

```typescript
import { Injectable } from '@nestjs/common';
import { CacheEvict } from 'tricache/nestjs';
import { Inject, Injectable } from '@nestjs/common';
import type { CacheService } from 'tricache';
import { CacheEvict, TRICACHE_SERVICE } from 'tricache/nestjs';

@Injectable()
export class UserService {
constructor(
@Inject(TRICACHE_SERVICE) readonly cacheService: CacheService,
) {}

@CacheEvict({
key: (userId: string) => `user:${userId}`,
tags: ['users'],
Expand All @@ -105,15 +131,18 @@ export class UserService {

## Direct `CacheService` Injection

Inject `CacheService` directly into services and controllers:
Inject the `TRICACHE_SERVICE` token (the module does not bind the `CacheService` class itself):

```typescript
import { Injectable } from '@nestjs/common';
import { CacheService } from 'tricache';
import { Inject, Injectable } from '@nestjs/common';
import type { CacheService } from 'tricache';
import { TRICACHE_SERVICE } from 'tricache/nestjs';

@Injectable()
export class OrderService {
constructor(private readonly cache: CacheService) {}
constructor(
@Inject(TRICACHE_SERVICE) private readonly cache: CacheService,
) {}

async processOrder(orderId: string) {
return await this.cache.lock(`order:${orderId}`, async () => {
Expand All @@ -123,3 +152,27 @@ export class OrderService {
}
}
```

## `@nestjs/cache-manager` store (`CACHE_MANAGER`)

`TriCacheModule` also exports `CACHE_MANAGER` bound to `TriCacheStore`. That adapter implements the cache-manager v5/v6 `CacheStore` contract (`get` / `set` / `del` / `reset`, plus `mget` / `mset` / `mdel` / `keys` / `ttl`). `set(key, value, ttl)` and `ttl(key)` use **milliseconds**.

```typescript
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER, type TriCacheStore } from 'tricache/nestjs';

@Injectable()
export class SessionService {
constructor(
@Inject(CACHE_MANAGER) private readonly cacheStore: TriCacheStore,
) {}

async save(id: string, value: unknown) {
await this.cacheStore.set(`session:${id}`, value, 60_000);
}

async load(id: string) {
return this.cacheStore.get(`session:${id}`);
}
}
```
6 changes: 6 additions & 0 deletions examples/nestjs-microservice/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
*.log
.pnpm-debug.log*
.DS_Store
*.tsbuildinfo
165 changes: 165 additions & 0 deletions examples/nestjs-microservice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# TriCache NestJS Microservice Demo

Minimal NestJS 11 TypeScript app that uses the official [`tricache/nestjs`](https://kareem411.github.io/TriCache/integrations/nestjs) entry:

| Surface | What to look for |
|---|---|
| `TriCacheModule.register({ ... })` | Wired in `AppModule` with in-process L1 options (Redis optional) |
| `@Cacheable({ ttl: 120, tags: ['items'] })` | `GET /items` and `GET /items/:id` reuse `computedAt` + `originReads` |
| `@CacheEvict({ tags: ['items'] })` | `POST` / `PATCH` / `DELETE` invalidate the `items` tag |
| `CACHE_MANAGER` / `TriCacheStore` | `PUT`/`GET`/`DELETE /store/notes/:id` — Nest cache-manager store contract |

The published decorator option is **`ttl` (seconds by default)**, plus optional `ttlUnit`. It is not `ttlSeconds` / `ttlSec`. Decorators resolve the engine from `this.cacheService` / `this.cache` / `this.cacheStore.cache`, so `ItemsService` injects `TRICACHE_SERVICE` onto `cacheService`.

Origin work is a simulated **250ms** catalog read. Cache hits replay the stored JSON and skip that delay.

Redis is not required. The demo uses an in-process L1 cache (`disableRedis: true`, `disableDisk: true`) unless `REDIS_HOST` is set.

---

## Run locally

From the **repository root**, build the local `tricache` package (the example links to `../..`):

```bash
pnpm install
pnpm build
```

Then start the demo:

```bash
cd examples/nestjs-microservice
pnpm install
pnpm dev
```

`pnpm start` is the same command. The process listens on `http://127.0.0.1:3000`. Override with `PORT` / `HOST` / `ORIGIN_LATENCY_MS`. Optional L2: `REDIS_HOST` / `REDIS_PORT`.

If you installed `tricache` from npm instead of the repo link, `node --import tsx src/main.ts` (or `pnpm dev`) is enough — no root build step.

---

## Try it with `curl`

Keep the server running in another terminal.

### 1. Cold miss — `@Cacheable`

```bash
curl -s 'http://127.0.0.1:3000/items/1'
```

Expect `originReads: 1`, a `computedAt` timestamp, and ~250ms. The service method ran.

### 2. Repeat GET — cache hit

```bash
curl -s 'http://127.0.0.1:3000/items/1'
```

Expect the **same** `computedAt` and `originReads: 1`, and a much faster response. `@Cacheable` served `item:1` from TriCache.

`GET /items` is a second key (`items:list`) with the same `items` tag.

### 3. Mutation — `@CacheEvict({ tags: ['items'] })`

```bash
curl -s -X PATCH 'http://127.0.0.1:3000/items/1' \
-H 'content-type: application/json' \
-d '{"name":"Ortho Keyboard","price":149}'
```

Expect `evictedTags: ["items"]`.

### 4. GET after eviction — miss and refill

```bash
curl -s 'http://127.0.0.1:3000/items/1'
```

Expect a **new** `computedAt`, `originReads: 2` (or higher if you also fetched the list), and `data.name: "Ortho Keyboard"`.

`GET /items` also misses — tag invalidation drops every key tagged `items`.

### 5. `@nestjs/cache-manager` store (`CACHE_MANAGER` → `TriCacheStore`)

`set` / `ttl` use **milliseconds**, matching cache-manager v5/v6:

```bash
curl -s 'http://127.0.0.1:3000/store/notes/n1'
# {"key":"note:n1","note":null,"cache":"miss"}

curl -s -X PUT 'http://127.0.0.1:3000/store/notes/n1' \
-H 'content-type: application/json' \
-d '{"body":"session-token","ttlMs":60000}'

curl -s 'http://127.0.0.1:3000/store/notes/n1'
# {"cache":"hit","note":{"id":"n1","body":"session-token",...},"ttlMs":...}

curl -s -X DELETE 'http://127.0.0.1:3000/store/notes/n1'
curl -s 'http://127.0.0.1:3000/store/notes/n1'
# {"cache":"miss"}
```

`GET /store/keys` lists keys currently resident in `TriCacheStore`.

---

## Automated check

```bash
pnpm verify
```

Starts the server on port `34568` and asserts cacheable hit/miss, tag eviction, and the store read/write path.

```bash
pnpm typecheck
```

---

## How the module is wired

```typescript
import { Module } from '@nestjs/common';
import { TriCacheModule } from 'tricache/nestjs';

@Module({
imports: [
TriCacheModule.register({
namespace: 'nestjs-microservice-demo',
disableRedis: true,
disableDisk: true,
invalidationBackplane: false,
}),
],
})
export class AppModule {}
```

`register()` accepts `CacheOptions` and calls `CacheService.create(options)`. The dynamic module is always `global: true` and exports:

`tricache/nestjs` types a structural `DynamicModule` so `@nestjs/common` can stay an optional peer. The example casts that object to Nest's `DynamicModule` at the `AppModule` boundary — runtime shape is unchanged.

- `TRICACHE_SERVICE` — `CacheService` instance
- `CACHE_MANAGER` — `TriCacheStore` (same token string as `@nestjs/cache-manager`)

```typescript
@Inject(TRICACHE_SERVICE) readonly cacheService: CacheService
@Inject(CACHE_MANAGER) readonly cacheStore: TriCacheStore
```

```typescript
@Cacheable({ key: (id: string) => `item:${id}`, ttl: 120, ttlUnit: 'seconds', tags: ['items'] })
async findOne(id: string) { /* origin read */ }

@CacheEvict({ tags: ['items'] })
async update(id: string, patch: ItemMutation) { /* mutation */ }

await this.cacheStore.set('note:n1', note, 60_000); // milliseconds
await this.cacheStore.get('note:n1');
```

`registerAsync({ useFactory, inject })` is available for `ConfigService`; this demo uses `register()` so it runs without extra Nest config modules.
30 changes: 30 additions & 0 deletions examples/nestjs-microservice/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "nestjs-microservice",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "TriCache NestJS 11 demo: TriCacheModule.register, @Cacheable, @CacheEvict, and CACHE_MANAGER / TriCacheStore",
"scripts": {
"dev": "tsx src/main.ts",
"start": "tsx src/main.ts",
"typecheck": "tsc --noEmit",
"verify": "tsx src/verify.ts"
},
"dependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"tricache": "link:../.."
},
"devDependencies": {
"@types/node": "^22.18.0",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=20.10.0"
},
"packageManager": "pnpm@11.22.0"
}
Loading
Loading