Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions docs/integrations/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

TriCache provides enterprise-grade HTTP route caching middleware with weak ETag calculation, deterministic query sorting, and RFC 7232 `304 Not Modified` short-circuiting for Express, Fastify, Connect, and Node.js HTTP servers.

### Ready-to-run Express demo

A self-contained microservice lives at [`examples/express-api`](https://github.com/Kareem411/TriCache/tree/main/examples/express-api). It exercises weak ETags, `If-None-Match` → `304`, deterministic query sorting, `headerWhitelist: ['accept-language']`, and `skipCache` for `Authorization`.

```bash
pnpm install && pnpm build
cd examples/express-api
pnpm install
pnpm dev
```

Then follow the `curl -i` walkthrough in that README.

---

## 1. Express & Connect (`createExpressMiddleware`)
Expand All @@ -21,9 +34,10 @@ const cache = CacheService.create();
// Route-level caching with automatic 304 Not Modified
app.get(
'/api/products',
createExpressMiddleware(cache, {
ttlSeconds: 300,
swrSeconds: 60,
createExpressMiddleware({
cache,
ttl: 300,
swr: 60,
headerWhitelist: ['accept-language'],
tags: ['products'],
}),
Expand All @@ -50,8 +64,9 @@ const fastify = Fastify();
const cache = CacheService.create();

// Register globally across all GET routes
await fastify.register(createFastifyPlugin(cache, {
ttlSeconds: 120,
await fastify.register(createFastifyPlugin({
cache,
ttl: 120,
headerWhitelist: ['x-tenant-id'],
}));
```
Expand All @@ -60,7 +75,7 @@ await fastify.register(createFastifyPlugin(cache, {
```typescript
import { createFastifyPlugin } from 'tricache/http';

const plugin = createFastifyPlugin(cache, { ttlSeconds: 300 });
const plugin = createFastifyPlugin({ cache, ttl: 300 });

fastify.get('/api/catalog', {
preHandler: plugin.preHandler,
Expand Down Expand Up @@ -99,8 +114,9 @@ TriCache respects standard HTTP client and server cache control semantics:
```typescript
app.get(
'/api/search',
createExpressMiddleware(cache, {
ttlSeconds: 60,
createExpressMiddleware({
cache,
ttl: 60,
skipCache: (req) => Boolean(req.headers['authorization']),
}),
searchHandler
Expand All @@ -115,8 +131,10 @@ app.get(

| Option | Type | Default | Description |
|---|---|---|---|
| `ttlSeconds` | `number` | `300` | Time-to-live in seconds |
| `swrSeconds` | `number` | `0` | Stale-While-Revalidate window in seconds |
| `cache` | `CacheService` | singleton | TriCache instance. If omitted, lazily resolves `CacheService.create()` |
| `ttl` | `number` | `300` | Time-to-live in seconds |
| `swr` | `number` | `undefined` | Stale-While-Revalidate window in seconds |
| `etag` | `boolean` | `true` | Generate and evaluate weak ETags (`W/"…"`) |
| `keyGenerator` | `(req) => string` | `buildDeterministicKey` | Custom cache key generator function |
| `headerWhitelist` | `string[]` | `[]` | Request headers incorporated into the cache key |
| `skipCache` | `(req) => boolean` | `undefined` | Predicate returning true to bypass cache |
Expand Down
6 changes: 6 additions & 0 deletions examples/express-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
*.log
.pnpm-debug.log*
.DS_Store
*.tsbuildinfo
143 changes: 143 additions & 0 deletions examples/express-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# TriCache Express API Demo

Minimal Express microservice that uses [`createExpressMiddleware`](../../src/http/express.ts) from [`tricache/http`](https://kareem411.github.io/TriCache/integrations/http).

It shows the five behaviors from [Kareem411/TriCache#25](https://github.com/Kareem411/TriCache/issues/25):

| Behavior | What to look for |
|---|---|
| Weak ETag | `ETag: W/"…"` on `200` responses |
| RFC 7232 `304` | Repeat with `If-None-Match` → empty `304 Not Modified` |
| Deterministic query sorting | `?limit=5&page=2` and `?page=2&limit=5` share `generatedAt` + ETag |
| `headerWhitelist: ['accept-language']` | `en` vs `fr` are separate cache entries |
| `skipCache` for auth | `Authorization: Bearer …` always hits origin (no ETag, new `generatedAt`) |

Origin work is a simulated **350ms** catalog query. Cache hits replay the stored JSON and skip that delay. Hits do **not** replay `X-TriCache-Demo: origin` — that header is set only when the route handler runs.

Redis is not required. The demo uses an in-process L1 cache (`disableRedis: true`, `disableDisk: true`).

---

## Run locally

From the **repository root**, build the local `tricache` package (the example links to `../..`):

```bash
pnpm install
pnpm build
```

Then start the demo:

```bash
cd examples/express-api
pnpm install
pnpm dev
```

`pnpm start` is the same command. The process listens on `http://127.0.0.1:3000`. Override with `PORT` / `HOST` / `ORIGIN_LATENCY_MS`.

If you installed `tricache` from npm instead of the repo link, `node --import tsx src/server.ts` (or `pnpm dev`) is enough — no root build step.

---

## Try it with `curl -i`

Keep the server running in another terminal. Use an explicit `Accept-Language`: an omitted language header and `Accept-Language: en` are **different** cache keys (the whitelist only adds the header when it is present).

### 1. Cold miss — weak ETag

```bash
curl -i 'http://127.0.0.1:3000/api/products?limit=5&page=2' \
-H 'Accept-Language: en'
```

Expect `HTTP/1.1 200`, `ETag: W/"…"`, `X-TriCache-Demo: origin`, and a `generatedAt` timestamp. This request takes ~350ms.

### 2. Same page, swapped query — cache hit

```bash
curl -i 'http://127.0.0.1:3000/api/products?page=2&limit=5' \
-H 'Accept-Language: en'
```

Expect the **same** `ETag` and `generatedAt`, no `X-TriCache-Demo` header, and a much faster response. TriCache sorts query parameters before hashing the key.

### 3. Conditional GET — `304 Not Modified`

Capture the ETag from a **GET** (`curl -sI` is HEAD, and HEAD is a different cache key):

```bash
ETAG=$(curl -sD - -o /dev/null 'http://127.0.0.1:3000/api/products?limit=5&page=2' \
-H 'Accept-Language: en' \
| awk -F': ' 'tolower($1)=="etag"{gsub("\r","",$2); print $2}')

curl -i 'http://127.0.0.1:3000/api/products?limit=5&page=2' \
-H 'Accept-Language: en' \
-H "If-None-Match: $ETAG"
```

Expect `HTTP/1.1 304 Not Modified`, the same `ETag`, and an **empty** body.

### 4. Language variants — `headerWhitelist`

```bash
curl -i 'http://127.0.0.1:3000/api/products?limit=5&page=2' \
-H 'Accept-Language: fr'
```

Expect a new origin fetch (`X-TriCache-Demo: origin`), a **different** ETag, `lang: "fr"`, and localized names (for example `Haut-parleurs de bureau`). `User-Agent` and other non-whitelisted headers do not fragment the cache.

### 5. Authenticated request — `skipCache`

```bash
curl -i 'http://127.0.0.1:3000/api/products?limit=5&page=2' \
-H 'Accept-Language: en' \
-H 'Authorization: Bearer demo'
```

Expect `cacheBypassed: true`, `X-TriCache-Demo: origin`, **no** `ETag`, and a new `generatedAt` on every call. Repeat the same command to confirm the timestamp changes.

`Cache-Control: no-cache` / `no-store` also bypass the cache (built into `tricache/http`).

---

## Automated check

```bash
pnpm verify
```

Starts the server on port `34567` and asserts the five behaviors above.

---

## How the middleware is wired

```typescript
import { CacheService } from 'tricache';
import { createExpressMiddleware } from 'tricache/http';

const cache = CacheService.create({
namespace: 'express-api-demo',
disableRedis: true,
disableDisk: true,
invalidationBackplane: false,
});

app.get(
'/api/products',
createExpressMiddleware({
cache,
ttl: 120,
swr: 30,
etag: true,
tags: ['products'],
headerWhitelist: ['accept-language'],
skipCache: (req) => Boolean(req.headers.authorization),
}),
productsHandler
);
```

The published options object is `{ cache, ttl, swr, etag, tags, headerWhitelist, skipCache }` — not `(cache, { ttlSeconds })`.
27 changes: 27 additions & 0 deletions examples/express-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "express-api",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "TriCache Express microservice demo: weak ETag, 304 Not Modified, query sorting, headerWhitelist, skipCache",
"scripts": {
"dev": "tsx src/server.ts",
"start": "tsx src/server.ts",
"typecheck": "tsc --noEmit",
"verify": "tsx src/verify.ts"
},
"dependencies": {
"express": "^4.21.2",
"tricache": "link:../.."
},
"devDependencies": {
"@types/express": "^4.17.23",
"@types/node": "^22.18.0",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=20.10.0"
},
"packageManager": "pnpm@11.22.0"
}
Loading