원클릭으로
salvo-caching
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
Create basic Salvo web applications with handlers, routers, and server setup. Use when starting a new Salvo project or adding basic HTTP endpoints.
Compress HTTP responses using gzip, brotli, zstd, or deflate. Use for reducing bandwidth and improving load times.
Limit concurrent requests to protect resources. Use for file uploads, expensive operations, and preventing resource exhaustion.
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
Implement CSRF (Cross-Site Request Forgery) protection using cookie or session storage. Use for protecting forms and state-changing endpoints.
| name | salvo-caching |
| description | Implement caching strategies for improved performance. Use for reducing database load and speeding up responses. |
| version | 0.94.0 |
| tags | ["performance","caching","cache-control","etag"] |
[dependencies]
salvo = { version = "0.94.0", features = ["cache", "caching-headers"] }
The cache feature activates salvo-cache with the default moka-store backend.
salvo::cache::Cache caches full responses (status, headers, body). By default it caches GET only and skips streaming responses and error bodies.
use std::time::Duration;
use salvo::cache::{Cache, MokaStore, RequestIssuer};
use salvo::prelude::*;
#[handler]
async fn expensive() -> String {
format!("computed at {}", chrono::Utc::now())
}
#[tokio::main]
async fn main() {
let cache = Cache::new(
MokaStore::builder()
.time_to_live(Duration::from_secs(60))
.max_capacity(10_000)
.build(),
RequestIssuer::default(),
);
let router = Router::new().hoop(cache).get(expensive);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Cache::new(store, issuer) is the only constructor. Tunable:
.skipper(impl Skipper) — default is MethodSkipper::new().skip_all().skip_get(false) (GET only). Pass a closure |req, depot| bool to override.Builds cache keys from the request URI and method. Toggle parts:
let issuer = RequestIssuer::new()
.use_scheme(false)
.use_authority(false)
.use_path(true)
.use_query(true)
.use_method(true);
All five are true by default.
Implement CacheIssuer (or pass a closure) to vary cache keys by user, tenant, etc.:
use salvo::cache::CacheIssuer;
let issuer = |req: &mut Request, depot: &Depot| -> Option<String> {
let user_id = depot.get::<String>("user_id").ok()?;
Some(format!("{user_id}:{}", req.uri().path()))
};
let cache = Cache::new(store, issuer);
Returning None disables caching for that request.
salvo::caching_headers::CachingHeaders adds ETag and Last-Modified handling to downstream handlers, responding 304 when If-None-Match / If-Modified-Since match:
use salvo::caching_headers::CachingHeaders;
let router = Router::new()
.hoop(CachingHeaders::new())
.get(handler);
For ETag-only or Last-Modified-only, use ETag::new() or Modified::new() from the same module.
#[handler]
async fn cached(res: &mut Response) -> &'static str {
res.headers_mut().insert(
"cache-control",
"public, max-age=3600, stale-while-revalidate=86400".parse().unwrap(),
);
"hello"
}
Directives cheat sheet:
public, max-age=N — shared caches may store for N secondsprivate, max-age=N — browser onlyno-store — never cacheno-cache — must revalidate with originstale-while-revalidate=N — serve stale for N seconds while revalidatingStatusError has no not_modified(). Set the status directly:
#[handler]
async fn with_etag(req: &mut Request, res: &mut Response) {
let etag = compute_etag();
if req.header::<String>("if-none-match").as_deref() == Some(etag.as_str()) {
res.status_code(StatusCode::NOT_MODIFIED);
return;
}
res.headers_mut().insert("etag", etag.parse().unwrap());
res.render(Json(load_data().await));
}
For caching values inside handlers (not full responses), use moka::future::Cache directly and share via affix_state:
moka = { version = "0.12", features = ["future"] }
use moka::future::Cache as MokaCache;
use std::sync::Arc;
type UserCache = Arc<MokaCache<i64, User>>;
#[handler]
async fn get_user(req: &mut Request, depot: &mut Depot) -> Result<Json<User>, StatusError> {
let id = req.param::<i64>("id").ok_or_else(StatusError::bad_request)?;
let cache = depot.get_typed::<UserCache>().unwrap();
let pool = depot.get_typed::<PgPool>().unwrap();
if let Some(user) = cache.get(&id).await { return Ok(Json(user)); }
let user = sqlx::query_as::<_, User>("select id, name from users where id = $1")
.bind(id).fetch_optional(pool).await
.map_err(|_| StatusError::internal_server_error())?
.ok_or_else(StatusError::not_found)?;
cache.insert(id, user.clone()).await;
Ok(Json(user))
}
#[tokio::main]
async fn main() {
let cache: UserCache = Arc::new(
MokaCache::builder()
.max_capacity(1_000)
.time_to_live(std::time::Duration::from_secs(60))
.build(),
);
let router = Router::new()
.hoop(affix_state::inject(cache))
.push(Router::with_path("users/{id}").get(get_user));
// ...
}
Invalidate on write:
#[handler]
async fn update_user(req: &mut Request, depot: &mut Depot) -> StatusCode {
let id = req.param::<i64>("id").unwrap();
// update db...
depot.get_typed::<UserCache>().unwrap().invalidate(&id).await;
StatusCode::OK
}
invalidate_all() clears everything; iterate over IDs for bulk invalidation.
Cache is full-response. It needs a deterministic key — beware caching per-user data under a shared RequestIssuer; use a custom issuer keyed on user ID.ResBody::Stream) and error bodies are never cached, silently.Cache::new does not return a builder — configure the store via MokaStore::builder().MokaStore::builder() (not Cache::builder), RequestIssuer::default() or new().StatusError::not_modified() does NOT exist — set StatusCode::NOT_MODIFIED directly.Cache so the stored body is already gzipped.serve-static sets ETag/Last-Modified automatically.