Skip to content

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.

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))
);

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.

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),
}),
}),
});

A few things are happening here:

  1. The queries function receives your tags. This is how you get type-safe access to the tag builders you defined in your schema.

  2. 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.

  3. The fn is what actually fetches the data. It only runs on cache misses. The rest of the time, you get the cached value.

// These hit the database the first time, cache after that
await cache.getUser('123');
await cache.getPost('123', 'p1');
// User 123 changed? Invalidate everything under that tag
await cache.invalidate(cache.tags.users('123'));
// Next call to getUser('123') will re-fetch

That’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.

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 :)