Skip to content

Caching

BananaJS provides method-level caching via @Cache and @CacheEvict decorators backed by CacheManager. The built-in store is in-memory with TTL support; plug in Redis or any custom backend via the CacheStore interface.

Setup

No extra install needed for in-memory caching. Pass cache in BananaAppOptions:

typescript
import { BananaApp, defineBananaControllers } from '@banana-universe/bananajs'

new BananaApp({
  controllers: defineBananaControllers(ProductController),
  cache: {
    store: 'memory',    // default — in-process LRU with TTL
  },
})

For Redis, install a Redis CacheStore adapter and pass it:

typescript
import { RedisCacheStore } from './cache/RedisCacheStore.js'  // your adapter

new BananaApp({
  controllers: defineBananaControllers(ProductController),
  cache: { store: new RedisCacheStore({ url: process.env.REDIS_URL }) },
})

@Cache(options) — cache the return value

Caches the method's return value under a key for a specified TTL. On subsequent calls within the TTL, the method is not invoked and the cached value is returned.

typescript
import { Cache, Controller, Get, BaseController } from '@banana-universe/bananajs'
import type { Request, Response } from 'express'

@Controller('products')
export class ProductController extends BaseController {

  // Cache each product by its ID for 5 minutes
  @Get(':id')
  @Cache({ ttl: 300, key: (req) => `product:${req.params.id}` })
  async getProduct(req: Request, res: Response) {
    const product = await this.service.findById(req.params.id)
    if (!product) throw new NotFoundError('Product not found')
    return this.ok(res, 'Product found', product)
  }

  // Cache the full product list for 1 minute
  @Get('')
  @Cache({ ttl: 60, key: 'products:list' })
  async listProducts(_req: Request, res: Response) {
    const products = await this.service.findAll()
    return this.ok(res, 'Products fetched', products)
  }
}

Options:

OptionTypeDescription
ttlnumberSeconds until expiry
keystring | ((req: Request) => string)Cache key or factory function

@CacheEvict(options) — invalidate on mutation

Clears cache entries matching a key or glob pattern after the method completes successfully.

typescript
@Controller('products')
export class ProductController extends BaseController {

  // Evict the specific product and the list on update
  @Put(':id')
  @Body(UpdateProductSchema)
  @CacheEvict({ pattern: `product:*` })    // clears all product:* keys
  async updateProduct(req: Request, res: Response) {
    const updated = await this.service.update(req.params.id, req.body)
    return this.ok(res, 'Updated', updated)
  }

  // Evict just the list cache on create
  @Post('')
  @Body(CreateProductSchema)
  @CacheEvict({ key: 'products:list' })
  async createProduct(req: Request, res: Response) {
    const product = await this.service.create(req.body)
    return this.ok(res, 'Created', product)
  }

  // Evict everything on bulk delete
  @Delete('')
  @CacheEvict({ pattern: '*' })
  async clearAll(req: Request, res: Response) {
    await this.service.deleteAll()
    return this.ok(res, 'Cleared', null)
  }
}

Options:

OptionTypeDescription
keystringExact key to evict
patternstringGlob pattern — product:* evicts all keys starting with product:

Use key for precise invalidation. Use pattern when a write affects multiple cached entries.

CacheManager — programmatic access

Use CacheManager directly in services when the decorator approach doesn't fit (e.g. conditional caching based on business logic):

typescript
import { CacheManager } from '@banana-universe/bananajs'

@injectable()
export class ProductService {
  private cache = CacheManager.getInstance()

  async findById(id: string): Promise<Product | null> {
    const cacheKey = `product:${id}`
    const cached = await this.cache.get<Product>(cacheKey)
    if (cached) return cached

    const product = await this.repo.findById(id)
    if (product) {
      await this.cache.set(cacheKey, product, 300)  // ttl in seconds
    }
    return product
  }

  async update(id: string, data: Partial<Product>): Promise<Product> {
    const product = await this.repo.update(id, data)
    await this.cache.delete(`product:${id}`)
    return product
  }
}

CacheManager API:

MethodSignatureDescription
get<T>(key: string) => Promise<T | null>Retrieve cached value
set(key, value, ttl?) => Promise<void>Store value; ttl seconds or no expiry
delete(key: string) => Promise<void>Remove one key
evict(pattern: string) => Promise<void>Remove all keys matching glob pattern
clear() => Promise<void>Remove all entries

Custom CacheStore

Implement CacheStore to use Redis, Memcached, or any other backend:

typescript
import type { CacheStore } from '@banana-universe/bananajs'
import { createClient } from 'redis'

export class RedisCacheStore implements CacheStore {
  private client = createClient({ url: process.env.REDIS_URL })

  async connect() {
    await this.client.connect()
  }

  async get<T>(key: string): Promise<T | null> {
    const raw = await this.client.get(key)
    return raw ? (JSON.parse(raw) as T) : null
  }

  async set(key: string, value: unknown, ttl?: number): Promise<void> {
    const serialized = JSON.stringify(value)
    if (ttl) {
      await this.client.setEx(key, ttl, serialized)
    } else {
      await this.client.set(key, serialized)
    }
  }

  async delete(key: string): Promise<void> {
    await this.client.del(key)
  }

  async evict(pattern: string): Promise<void> {
    const keys = await this.client.keys(pattern)
    if (keys.length > 0) await this.client.del(keys)
  }

  async clear(): Promise<void> {
    await this.client.flushDb()
  }
}

Pass to BananaApp:

typescript
const redisStore = new RedisCacheStore()
await redisStore.connect()

new BananaApp({
  controllers: defineBananaControllers(ProductController),
  cache: { store: redisStore },
})

Multi-tenant cache isolation

When using @Tenant(), cache keys are automatically namespaced by tenant ID. Cached data from tenant-A is never returned to tenant-B:

typescript
@Controller('products')
@Tenant()
export class ProductController extends BaseController {
  @Get(':id')
  @Cache({ ttl: 300, key: (req) => `product:${req.params.id}` })
  async getProduct(req: Request, res: Response) {
    // Key is stored as `<tenantId>:product:<id>` — no cross-tenant leakage
  }
}

For manual keys via CacheManager, use getTenantId() to namespace:

typescript
import { getTenantId } from '@banana-universe/bananajs'

const key = `${getTenantId()}:product:${id}`

Released under the MIT License.