Tutorial
Let’s build a cache for a small app with users, posts, and comments. By the end of this page, you’ll understand how QueryCache works, why the pieces fit together the way they do, and how to apply it to your own app.
Unlike this documentation’s sardonic, snarky tone peppered with self-indulgent dad jokes , we’ve really tried here to make this tutorial friendly, approachable, and usable as soon as you’re done making your way through it. Let’s get started.
Step 1: Define your schema
Section titled “Step 1: Define your schema”The schema tells t87s what your tag hierarchy looks like. Think of it as a map of how your data relates to itself.
import { at, wild } from '@t87s/core';
const schema = at('users', () => wild.at('posts', () => wild.at('comments', () => wild)));from t87s import TagSchema, Wild
class Tags(TagSchema): users: Wild["Users"]
class Users(TagSchema): posts: Wild["Posts"]
class Posts(TagSchema): comments: Wild[TagSchema]This says: there’s a users namespace, and each user (that’s what wild means—a dynamic ID) can have posts, and each post can have comments. When you invalidate a user, all their posts and comments go stale too. When you invalidate just a post, only that post and its comments go stale.
Step 2: Create your cache
Section titled “Step 2: Create your cache”import { QueryCache, at, wild, MemoryAdapter } from '@t87s/core';
const cache = QueryCache({ schema, adapter: new MemoryAdapter(), queries: (tags) => ({ getUser: (id: string) => ({ tags: [tags.users(id)], fn: () => db.users.findById(id), }), getPost: (userId: string, postId: string) => ({ tags: [tags.users(userId).posts(postId)], fn: () => db.posts.findById(postId), }), }),});from t87s import QueryCache, cachedfrom t87s.adapters import AsyncMemoryAdapter
class Cache(QueryCache[Tags]): @cached(Tags.users()) async def get_user(self, id: str): return await db.users.find_by_id(id)
@cached(Tags.users().posts()) async def get_post(self, user_id: str, post_id: str): return await db.posts.find_by_id(post_id)
cache = Cache(adapter=AsyncMemoryAdapter())A few things are happening here:
-
The
queriesfunction receives your tags. This is how you get type-safe access to the tag builders you defined in your schema. -
Each query specifies its tags. When you call
getUser('123'), the result gets tagged with['users', '123']. Later, when you invalidate that tag, this cached result goes stale. -
The
fnis what actually fetches the data. It only runs on cache misses. The rest of the time, you get the cached value.
Step 3: Use it
Section titled “Step 3: Use it”// These hit the database the first time, cache after thatawait cache.getUser('123');await cache.getPost('123', 'p1');
// User 123 changed? Invalidate everything under that tagawait cache.invalidate(cache.tags.users('123'));
// Next call to getUser('123') will re-fetch# These hit the database the first time, cache after thatawait cache.get_user("123")await cache.get_post("123", "p1")
# User 123 changed? Invalidate everything under that tagawait cache.invalidate(cache.t.users("123"))
# Next call to get_user("123") will re-fetchThat’s all, folks! Query, cache, invalidate, repeat. The cache handles all the fun parts—storing values, checking freshness, managing TTLs—so you can focus on the boring stuff, like profit.
What’s next
Section titled “What’s next”If you want to understand the schema system better, check out Schema. If you want to know about TTL and grace periods, check TTL and Grace Periods. And if you just want to ship something, you have enough to get started. The rest is for what we call in Finnish “pilkunviilaaja”. We’ll let you ChatGPT that :)