Skip to main content
You can set Cache-Control headers on API responses by adding the cacheControl option to Sonamu’s @api decorator.

Basic Usage

Adding to @api Decorator

import { BaseModel, api, CachePresets } from "sonamu";

class ProductModelClass extends BaseModel {
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.shortLived,  // 1 minute cache
  })
  async findAll() {
    return this.findMany({});
  }
}
Result:
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Content-Type: application/json

[{"id": 1, "name": "Product A"}, ...]

Configuration Methods

The simplest approach
@api({
  httpMethod: 'GET',
  cacheControl: CachePresets.mediumLived,  // 5 minute cache
})
async getCategories() {
  return this.categories;
}
Available presets:
  • noStore: No caching
  • noCache: Revalidate every time
  • shortLived: 1 minute
  • mediumLived: 5 minutes
  • longLived: 1 hour
  • private: Personalized data

Priority Order

Cache-Control settings are applied in the following order:
  1. @api decorator’s cacheControl (highest priority)
  2. cacheControlHandler return value
  3. Default (no Cache-Control header)

Example

// sonamu.config.ts
cacheControlHandler: (req) => {
  if (req.type === 'api' && req.method === 'GET') {
    return CachePresets.shortLived;  // Default 1 minute
  }
}

// Model
@api({
  httpMethod: 'GET',
  cacheControl: CachePresets.mediumLived,  // Override to 5 minutes
})
async getData() { ... }
Result: mediumLived applied (decorator takes priority)

Strategies by HTTP Method

GET Requests

Read APIs can be cached.
// Post list (frequently added)
@api({
  httpMethod: 'GET',
  cacheControl: CachePresets.shortLived,  // 1 minute
})
async getPosts(page: number) {
  return this.findMany({ page, num: 20 });
}

POST, PUT, DELETE Requests

Mutation requests should not be cached.
// ✅ Correct example
@api({
  httpMethod: 'POST',
  cacheControl: CachePresets.noStore,  // No caching
})
async create(data: ProductSave) {
  return this.saveOne(data);
}

@api({
  httpMethod: 'PUT',
  cacheControl: CachePresets.noStore,
})
async update(id: number, data: Partial<ProductSave>) {
  return this.updateOne(['id', id], data);
}

@api({
  httpMethod: 'DELETE',
  cacheControl: CachePresets.noStore,
})
async delete(id: number) {
  return this.deleteOne(['id', id]);
}
Why you should not cache mutation requests:
  • Sending the same POST request multiple times causes duplicate creations
  • Browser reuses previous response → appears as if nothing was actually created
// ❌ Never do this
@api({
  httpMethod: 'POST',
  cacheControl: { maxAge: 60 },  // Dangerous!
})
async create(data: ProductSave) { ... }

Strategies by Data Characteristics

Public Data (public)

Same response for all users.
class ProductModelClass extends BaseModel {
  // Product list
  @api({
    httpMethod: 'GET',
    cacheControl: {
      visibility: 'public',  // CDN cacheable
      maxAge: 60,
    }
  })
  async findAll() {
    return this.findMany({});
  }
}
Characteristics:
  • Cached by browser + CDN
  • Minimizes network traffic
  • Reduces server load

Private Data (private)

Different response for each user.
class OrderModelClass extends BaseModel {
  // My order history
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.private,  // Browser only caching
  })
  async getMyOrders(ctx: Context) {
    return this.findMany({
      where: [['user_id', ctx.user.id]]
    });
  }
}
Characteristics:
  • Cached only in browser (not CDN)
  • Prevents exposure to other users

Sensitive Data (no-store)

Data that should not be cached.
class PaymentModelClass extends BaseModel {
  // Payment information
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.noStore,  // No caching
  })
  async getPaymentInfo(id: number) {
    return this.findOne(['id', id]);
  }
}

Practical Examples

1. E-commerce API

class ProductModelClass extends BaseModel {
  // Product list (frequently changed, public)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.shortLived,  // 1 minute
  })
  async findAll(page: number) {
    return this.findMany({ page, num: 20 });
  }

  // Product detail (occasionally changed, public)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.mediumLived,  // 5 minutes
  })
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

  // Categories (rarely changed, public)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.longLived,  // 1 hour
  })
  async getCategories() {
    return this.categories;
  }

  // Stock (real-time, no-cache)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.noCache,  // Revalidate every time
  })
  async getStock(productId: number) {
    return this.checkStock(productId);
  }

  // Shopping cart (personal, private)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.private,  // Browser only
  })
  async getMyCart(ctx: Context) {
    return this.getCart(ctx.user.id);
  }

  // Order creation (Mutation, no-store)
  @api({
    httpMethod: 'POST',
    cacheControl: CachePresets.noStore,  // No caching
  })
  async createOrder(data: OrderSave) {
    return this.saveOne(data);
  }
}

2. Blog API

class PostModelClass extends BaseModel {
  // Post list (frequently added, 5 minutes)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.mediumLived,
  })
  async findAll(page: number) {
    return this.findMany({ page, num: 10 });
  }

  // Post detail (occasionally modified, 10 minutes)
  @api({
    httpMethod: 'GET',
    cacheControl: {
      visibility: 'public',
      maxAge: 600,  // 10 minutes
      staleWhileRevalidate: 1800,  // Use stale for 30 minutes
    }
  })
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

  // Popular posts (1 hour)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.longLived,
  })
  async getPopular() {
    return this.findPopular();
  }
}

3. User API

class UserModelClass extends BaseModel {
  // Public profile (public, 5 minutes)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.mediumLived,
  })
  async getPublicProfile(username: string) {
    return this.findOne(['username', username]);
  }

  // My profile (private, revalidate)
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.private,
  })
  async getMyProfile(ctx: Context) {
    return this.findOne(['id', ctx.user.id]);
  }

  // Profile update (no-store)
  @api({
    httpMethod: 'PUT',
    cacheControl: CachePresets.noStore,
  })
  async updateProfile(ctx: Context, data: Partial<UserSave>) {
    return this.updateOne(['id', ctx.user.id], data);
  }
}

CDN Optimization

Using s-maxage

You can apply different TTLs for browser and CDN.
@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'public',
    maxAge: 60,       // Browser: 1 minute
    sMaxAge: 300,     // CDN: 5 minutes
  }
})
async getData() {
  return this.findMany({});
}
Effect:
  • Browser: Requests CDN every minute
  • CDN: Requests server every 5 minutes
  • Minimizes server load

Stale-While-Revalidate

CDN returns stale response immediately while updating in background:
@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'public',
    maxAge: 60,
    staleWhileRevalidate: 300,  // Allow stale for 5 minutes
  }
})
async getProducts() {
  return this.findMany({});
}
CloudFront Support: AWS CloudFront supports stale-while-revalidate.

Vary Header

Uses different caches based on request headers.
@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'public',
    maxAge: 300,
    vary: ['Accept-Language'],  // Separate cache by language
  }
})
async getProducts(ctx: Context) {
  const locale = ctx.locale;  // 'ko' or 'en'
  return this.getLocalizedProducts(locale);
}
Result:
GET /api/products (Accept-Language: ko) → Korean response cached
GET /api/products (Accept-Language: en) → English response cached

Global Handler

You can apply Cache-Control to all APIs at once.
// sonamu.config.ts
import { CachePresets, type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        // Only handle API requests
        if (req.type !== 'api') {
          return undefined;
        }

        // Handle by HTTP method
        if (req.method !== 'GET') {
          return CachePresets.noStore;  // Mutations use no-store
        }

        // Handle by API path
        if (req.path.includes('/admin/')) {
          return CachePresets.noStore;  // Admin APIs use no-store
        }

        if (req.path.includes('/private/')) {
          return CachePresets.private;  // Private APIs use private
        }

        // Default: 1 minute cache
        return CachePresets.shortLived;
      }
    }
  }
};

Using API Object

cacheControlHandler: (req) => {
  if (req.type !== 'api') return undefined;

  // Use ExtendedApi information
  if (req.api) {
    const { modelName, methodName } = req.api;

    // Long cache for specific Model only
    if (modelName === 'ConfigModel') {
      return CachePresets.longLived;
    }

    // No caching for specific methods
    if (methodName.startsWith('create') || methodName.startsWith('update')) {
      return CachePresets.noStore;
    }
  }

  return CachePresets.shortLived;
}

Using with BentoCache

Combining server cache with HTTP cache provides maximum performance.
@cache({ ttl: "10m", tags: ["product"] })  // Server cache: 10 minutes
@api({
  httpMethod: 'GET',
  cacheControl: { maxAge: 60 }  // HTTP cache: 1 minute
})
async getProducts() {
  return this.findMany({});
}
Effect:
  1. 0-60 seconds: Browser cache (no server request)
  2. 60 seconds - 10 minutes: Server request but uses BentoCache (no DB query)
  3. After 10 minutes: DB query → cache again

Precautions

Precautions when using API Cache-Control:
  1. Only cache GET: POST, PUT, DELETE must use noStore
    // ❌ Dangerous
    @api({ httpMethod: 'POST', cacheControl: { maxAge: 60 } })
    
    // ✅ Safe
    @api({ httpMethod: 'POST', cacheControl: CachePresets.noStore })
    
  2. Private data must be private: Prevent CDN caching
    // ❌ Dangerous: Personal data cached on CDN
    @api({ cacheControl: { visibility: 'public', maxAge: 60 } })
    async getMyOrders(ctx: Context) { ... }
    
    // ✅ Safe
    @api({ cacheControl: CachePresets.private })
    async getMyOrders(ctx: Context) { ... }
    
  3. Short TTL for frequently changing data: Prevent stale data
    // ❌ Stock is real-time but cached for 1 hour
    @api({ cacheControl: CachePresets.longLived })
    async getStock() { ... }
    
    // ✅ Short TTL or no-cache
    @api({ cacheControl: CachePresets.noCache })
    async getStock() { ... }
    
  4. Be careful with authenticated APIs: Consider Authorization header
    @api({
      cacheControl: {
        visibility: 'private',  // private required
        maxAge: 60,
        vary: ['Authorization'],  // Separate cache by auth token
      }
    })
    async getMyData(ctx: Context) { ... }
    

Next Steps