FastAPI endpoints cached with cashews in-memory backend: working versions (tested 2026-10-15)
FastAPI routes backed by a cashews mem:// cache with key templates, TTL and explicit invalidation, tested fully offline through httpx ASGITransport.
passed6 passed · 0 failed2026-10-15 21:56 UTC- Status
- passed
- Last passed
- 2026-10-15 21:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38813530570
The code
Exactly the files that passed. Your agent can pull them with get_recipe("fastapi-cashews-memory-cache") (install), or see them on GitHub.
fastapi==0.143.2 cashews==7.6.0 httpx==0.28.1 pytest==9.1.1 pytest-asyncio==1.4.0
from fastapi import FastAPI, HTTPException
from cashews import cache
from cashews.contrib.fastapi import CacheRequestControlMiddleware
from pydantic import BaseModel
cache.setup("mem://")
class Item(BaseModel):
id: int
name: str
stock: int
class ItemNotFound(Exception):
pass
class _Store:
def __init__(self) -> None:
self.items: dict[int, Item] = {}
self.loads = 0
def reset(self) -> None:
self.items = {
1: Item(id=1, name="widget", stock=3),
2: Item(id=2, name="gadget", stock=9),
}
self.loads = 0
store = _Store()
store.reset()
app = FastAPI()
app.add_middleware(CacheRequestControlMiddleware)
@cache(ttl="5m", key="item:{item_id}")
async def load_item(item_id: int) -> Item:
store.loads += 1
try:
return store.items[item_id].model_copy(deep=True)
except KeyError:
raise ItemNotFound(item_id) from None
@app.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: int) -> Item:
try:
return await load_item(item_id)
except ItemNotFound:
raise HTTPException(status_code=404, detail="item not found")
async def set_stock(item_id: int, stock: int) -> Item:
item = store.items.get(item_id)
if item is None:
raise ItemNotFound(item_id)
item.stock = stock
await cache.delete(f"item:{item_id}")
return item.model_copy(deep=True)
@app.put("/items/{item_id}", response_model=Item)
async def put_item(item_id: int, stock: int) -> Item:
try:
return await set_stock(item_id, stock)
except ItemNotFound:
raise HTTPException(status_code=404, detail="item not found")
import httpx
import pytest
from httpx import ASGITransport
import app as app_module
from cashews import cache
@pytest.fixture(autouse=True)
async def fresh_state():
cache.setup("mem://")
app_module.store.reset()
yield
await cache.clear()
@pytest.fixture
async def client():
transport = ASGITransport(app=app_module.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
yield c
async def test_get_is_cached(client):
first = await client.get("/items/1")
second = await client.get("/items/1")
assert first.status_code == second.status_code == 200
assert first.json() == {"id": 1, "name": "widget", "stock": 3}
assert app_module.store.loads == 1
async def test_key_tracks_path_param(client):
await client.get("/items/1")
await client.get("/items/2")
assert app_module.store.loads == 2
async def test_update_invalidates_entry(client):
assert (await client.get("/items/1")).json()["stock"] == 3
updated = await client.put("/items/1", params={"stock": 10})
assert updated.json()["stock"] == 10
assert (await client.get("/items/1")).json()["stock"] == 10
assert app_module.store.loads == 2
async def test_missing_item_is_not_cached(client):
assert (await client.get("/items/99")).status_code == 404
assert (await client.get("/items/99")).status_code == 404
assert app_module.store.loads == 2
async def test_ttl_is_exposed(client):
await client.get("/items/1")
ttl = await cache.get_expire("item:1")
assert 0 < ttl <= 300
async def test_cache_control_header(client):
await client.get("/items/1")
hit = await client.get("/items/1")
assert hit.headers["cache-control"].startswith("private, max-age=")
assert int(hit.headers["age"]) > 0
[pytest] pythonpath = . asyncio_mode = auto
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
FastAPI endpoint with Pydantic v2 discriminated unions and Annotated validators
Accept a tagged union of payment methods with Field(discriminator), add Annotated validators, and test the exact 422 error shapes offline with TestClient.
FastAPI + Pydantic v2 validators, discriminated unions and serializers
Pydantic v2 patterns in FastAPI that agents often get wrong: field_validator, model_validator, discriminated unions, computed_field and response_model filtering.
FastAPI background tasks + dependency overrides
FastAPI BackgroundTasks for post-response work, with dependency_overrides to swap services in tests.
FastAPI config via pydantic-settings as a cached dependency
Load config from env vars with pydantic-settings, inject it into FastAPI routes through a cached dependency, and test it offline with monkeypatch and dependency_overrides.
All FastAPI recipesAll cashews recipesAll httpx recipesAll Pydantic recipes