Middleware & Cache
# Middleware
Create src/middleware.ts to run logic before every request:
import type { Middleware } from "@elurjs/kit";
const middleware: Middleware = (request, context) => {
if (!request.headers.get("Cookie")?.includes("session=")) {
return Response.redirect(new URL("/login", request.url), 307);
}
// Return nothing to continue to the route handler
};
export default middleware;
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
};# Middleware type
type Middleware = (
request: Request,
context: MiddlewareContext,
) => Response | void | Promise<Response | void>;# MiddlewareContext
| Field | Type | Description |
|---|---|---|
next(options?) |
(options?: { headers?, params?, locals? }) => void |
Continue to next handler with optional headers/params/locals |
params |
Record<string, string | string[]> | undefined |
Matched route params (if path matches a page route) |
locals |
Record<string, unknown> | undefined |
Per-request data, passed to actions and loaders |
# MiddlewareConfig
| Field | Type | Description |
|---|---|---|
matcher |
string[] |
Path patterns — * wildcard, :param segments |
# Middleware type
type Middleware = (
request: Request,
context: MiddlewareContext,
) => Response | void | Promise<Response | void>;# LoadedMiddleware
interface LoadedMiddleware {
handler: Middleware;
config: MiddlewareConfig;
}# MiddlewareResult
Tagged union returned by runMiddleware:
type MiddlewareResult =
| { kind: "response"; response: Response }
| {
kind: "continue";
headers?: Record<string, string>;
params?: Record<string, string | string[]>;
locals?: Record<string, unknown>;
};# Matching patterns
export const config = {
matcher: [
"/dashboard/:path*", // all dashboard routes
"/admin/*", // all admin routes
"/api/:method", // specific param
],
};# streamBoundary(options) — streaming content
Renders fallback content while a promise resolves, then swaps in the real content during streaming SSR:
import { streamBoundary } from "@elurjs/kit";
import { html } from "@elurjs/core";
html`
<h1>Blog Post</h1>
${streamBoundary({
fallback: html`<p>Loading comments…</p>`,
promise: fetchComments(postId),
children: (comments) => html`
<ul>${comments.map(c => html`<li>${c.text}</li>`)}</ul>
`,
})}
`# StreamBoundaryOptions<T>
| Field | Type | Description |
|---|---|---|
fallback |
ElurTemplate |
Content shown while promise resolves |
promise |
Promise<T> |
Promise that resolves to data |
children |
(value: T) => ElurTemplate |
Renders resolved value |
ℹ Note
Streaming is experimental. Some adapters may buffer the response instead of streaming.
# HTML cache
Legacy cache functions (from @elurjs/kit):
import { getCachedHtml, setCachedHtml, clearCache } from "@elurjs/kit";
// Check cache before rendering
const cached = await getCachedHtml(cacheDir, "/blog/hello-world");
if (cached) return cached;
// Cache after rendering
await setCachedHtml(cacheDir, "/blog/hello-world", html, 60);
// Clear all cache
await clearCache(cacheDir);# CacheEntry
interface CacheEntry {
html: string;
generatedAt: number;
revalidate: number;
}# Cache adapters
# Filesystem (default)
import { createFsCacheAdapter } from "@elurjs/kit";
const adapter = createFsCacheAdapter({
cacheDir: "./.elur/cache",
maxEntries: 1000, // default: 1000
maxAgeMs: 86_400_000, // default: 24h
});# Redis
import { createRedisCacheAdapter } from "@elurjs/kit";
const adapter = createRedisCacheAdapter({
client: redisClient, // ioredis, node-redis, or Upstash
prefix: "elur-kit:", // default prefix
});# Cloudflare KV
import { createCloudflareKvCacheAdapter } from "@elurjs/kit";
const adapter = createCloudflareKvCacheAdapter({
namespace: KV_NAMESPACE, // Cloudflare KV binding
});# CacheAdapter interface
interface CacheAdapter {
get(key: string): Promise<CacheEntry | null>;
set(key: string, value: CacheEntry, options: CacheWriteOptions): Promise<void>;
delete(key: string): Promise<void>;
invalidateTags(tags: readonly string[]): Promise<void>;
}# CacheWriteOptions
| Field | Type | Description |
|---|---|---|
revalidate |
number |
Revalidation seconds |
tags |
string[]? |
Tags for tag-based invalidation |
version |
string? |
Version string |
# Cache policy
Per-page cache policy via the loader — export cache (not cachePolicy):
// src/app/blog/[slug]/page.data.ts
export const load = async ({ params }) => {
const post = await db.posts.findBySlug(params.slug);
return { post };
};
export const cache = {
mode: "public", // "public" | "private" | "dynamic"
revalidate: 60, // seconds (0 = always revalidate)
tags: ["posts"], // for tag-based invalidation
};Default policy is dynamic (no caching). Requests with Cookie or
Authorization headers are never cached publicly.
| Mode | Description |
|---|---|
public |
CDN-cacheable, shared cache |
private |
Browser-only cache |
dynamic |
Never cache, always render on demand |
# Invalidation
# Tag-based
import { defaultInvalidator } from "@elurjs/kit";
await defaultInvalidator.invalidateTags(["posts"]);# Path-based
await defaultInvalidator.invalidatePaths(["/blog", "/blog/hello-world"]);# From actions
export const deletePost = defineAction(
{
invalidateTags: ["posts"],
invalidatePaths: ["/blog"],
},
async (input) => {
await db.posts.delete(input.id);
return { ok: true };
}
);# connectCacheAdapter(adapter, invalidator?)
Connects a cache adapter to the invalidation system. Returns an unsubscribe function:
import { connectCacheAdapter, defaultInvalidator } from "@elurjs/kit";
const adapter = createRedisCacheAdapter({ client: redisClient });
const unsubscribe = connectCacheAdapter(adapter, defaultInvalidator);
// later: unsubscribe() to disconnect# Low-level middleware utilities
# loadMiddleware(root?)
Loads src/middleware.ts and returns the loaded middleware with its config:
import { loadMiddleware } from "@elurjs/kit";
const loaded = await loadMiddleware("./src");
// loaded.handler, loaded.config
// Returns null if no middleware file exists# runMiddleware(middleware, request, params?)
import { runMiddleware } from "@elurjs/kit";
const result = await runMiddleware(loaded, request);
if (result.kind === "response") {
// middleware returned a redirect/error response
return result.response;
}
// result.kind === "continue"
// result.headers, result.params, result.locals# matchesMiddleware(pathname, config)
import { matchesMiddleware } from "@elurjs/kit";
matchesMiddleware("/dashboard/users", { matcher: ["/dashboard/:path*"] });
// → true