Extensions
Creating context-modifying extensions
The Extensions System allows you to add reusable context modifications to your API - logging, caching, monitoring, and more.
What are Extensions?
Extensions modify the context available to your queries and mutations:
- Add services (logging, caching, metrics)
- Add utility functions
- Add request-specific data
- Add cross-cutting concerns
Extensions do NOT add methods to t. The query() and mutation() methods are native to t.
Creating Extensions
Use extension():
import { extension } from '@deessejs/functions/extensions';
const loggingExtension = extension({
name: "logging",
// Initialize extension state
init: () => ({
logs: [] as string[],
requestCount: 0,
}),
// Add context for each request
request: (state, ctx) => ({
...ctx,
logs: state.logs,
requestCount: state.requestCount + 1,
log: (msg: string) => {
state.logs.push(`[${ctx.userId}] ${msg}`);
},
}),
});Context Extensions
Add services to context:
const cacheExtension = extension({
name: "caching",
init: () => ({
cache: new Map<string, any>(),
}),
request: (state, ctx) => ({
...ctx,
cache: state.cache,
get: (key: string) => state.cache.get(key),
set: (key: string, value: any) => state.cache.set(key, value),
}),
});
// Use in API
const { t, createAPI } = defineContext<{
userId: string;
}>().withExtensions([cacheExtension]);
// Access in handlers
const getUser = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
// ctx.get and ctx.set are available
const cached = ctx.get(`user:${args.id}`);
if (cached) return Result.success(cached);
const user = await ctx.db.find(args.id);
ctx.set(`user:${args.id}`, user);
return Result.success(user);
},
});Extension Lifecycle
init()
Initialize extension state (called once):
const metricsExtension = extension({
name: "metrics",
init: () => ({
counters: new Map<string, number>(),
timers: new Map<string, number[]>(),
}),
request: (state, ctx) => ({
...ctx,
metrics: {
increment: (name: string) => {
const current = state.counters.get(name) || 0;
state.counters.set(name, current + 1);
},
time: (name: string, duration: number) => {
const timings = state.timers.get(name) || [];
timings.push(duration);
state.timers.set(name, timings);
},
},
}),
});request()
Modify context for each request:
const authExtension = extension({
name: "auth",
request: async (state, ctx) => {
const user = await ctx.db.users.findById(ctx.userId);
if (!user) {
throw new Error('Unauthorized');
}
return {
...ctx,
user, // Add user to context
};
},
});Example Extensions
Logging Extension
const logging = extension({
name: "logging",
init: () => ({
logs: [] as string[],
requestCount: 0,
}),
request: (state, ctx) => ({
...ctx,
logs: state.logs,
requestCount: state.requestCount + 1,
log: (msg: string) => {
state.logs.push(`[${ctx.userId}] ${msg}`);
},
}),
});
// Usage
const { t, createAPI } = defineContext({ userId: "user-123" })
.withExtensions([logging]);
const getUser = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
ctx.log(`Fetching user ${args.id}`); // ← Added by logging extension
return Result.success(await ctx.db.users.find(args.id));
},
});Caching Extension
const caching = extension({
name: "caching",
init: () => ({
cache: new Map<string, any>(),
}),
request: (state, ctx) => ({
...ctx,
cache: state.cache,
get: (key: string) => state.cache.get(key),
set: (key: string, value: any) => state.cache.set(key, value),
}),
});
// Usage
const getUser = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
const cached = ctx.get(`users:${args.id}`);
if (cached) return Result.success(cached);
const user = await ctx.db.users.find(args.id);
ctx.set(`users:${args.id}`, user);
return Result.success(user);
},
});Authentication Extension
const authentication = extension({
name: "auth",
request: async (state, ctx) => {
const user = await ctx.db.users.findById(ctx.userId);
if (!user) {
throw new Error('Unauthorized');
}
return {
...ctx,
user, // Add user to context
};
},
});
// Usage
const { t, createAPI } = defineContext({ userId: "user-123" })
.withExtensions([authentication]);
const updateUser = t.mutation({
args: z.object({ name: z.string() }),
handler: async (ctx, args) => {
// ctx.user is available from auth extension
return Result.success(await ctx.db.users.update(ctx.user.id, args));
},
});Tracing Extension
const tracing = extension({
name: "tracing",
init: () => ({
traceId: 0,
}),
request: (state, ctx) => ({
...ctx,
traceId: ++state.traceId,
trace: (event: string) => {
console.log(`[trace:${state.traceId}] ${event}`);
},
}),
});Metrics Extension
const metrics = extension({
name: "metrics",
init: () => ({
counters: new Map<string, number>(),
}),
request: (state, ctx) => ({
...ctx,
increment: (name: string) => {
const current = state.counters.get(name) || 0;
state.counters.set(name, current + 1);
},
getCount: (name: string) => state.counters.get(name) || 0,
}),
});Combining Extensions
const { t, createAPI } = defineContext<{
userId: string;
}>()
.withExtensions([
logging,
caching,
metrics,
]);
// All context is merged
const getUser = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
// Available: ctx.userId, ctx.log, ctx.cache, ctx.increment
ctx.log("Fetching user");
ctx.increment("getUser.calls");
const cached = ctx.cache.get(`user:${args.id}`);
if (cached) return Result.success(cached);
const user = await ctx.db.users.find(args.id);
ctx.cache.set(`user:${args.id}`, user);
return Result.success(user);
},
});Extension Composition
Extensions compose automatically:
// Extension 1 adds logging
const logging = extension({
name: "logging",
request: (state, ctx) => ({
...ctx,
log: (msg: string) => console.log(msg),
}),
});
// Extension 2 adds caching
const caching = extension({
name: "caching",
request: (state, ctx) => ({
...ctx,
cache: new Map(),
}),
});
// Both context properties are available
const { t } = defineContext().withExtensions([logging, caching]);
// Handler has both log and cache
const query = t.query({
handler: async (ctx) => {
ctx.log("test"); // ← From logging
ctx.cache.get("key"); // ← From caching
},
});Configurable Extensions
Create reusable extensions with configuration:
const createCacheExtension = (options: {
defaultTTL?: number;
maxSize?: number;
}) => extension({
name: "caching",
init: () => ({
cache: new Map<string, { value: any; expires: number }>(),
}),
request: (state, ctx) => ({
...ctx,
get: (key: string) => {
const item = state.cache.get(key);
if (!item) return undefined;
if (item.expires < Date.now()) {
state.cache.delete(key);
return undefined;
}
return item.value;
},
set: (key: string, value: any, ttl?: number) => {
const expires = Date.now() + (ttl ?? options.defaultTTL ?? 300000);
// Enforce max size
if (options.maxSize && state.cache.size >= options.maxSize) {
const firstKey = state.cache.keys().next().value;
state.cache.delete(firstKey);
}
state.cache.set(key, { value, expires });
},
}),
});
// Usage with configuration
const { t } = defineContext()
.withExtensions([
createCacheExtension({ defaultTTL: 60000, maxSize: 1000 }),
]);Best Practices
1. Keep Extensions Focused
// ✅ Good - single responsibility
const logging = extension({ /* ... */ });
const caching = extension({ /* ... */ });
const metrics = extension({ /* ... */ });
// ❌ Bad - doing everything
const megaExtension = extension({
request: (state, ctx) => ({
...ctx,
logger, cache, metrics, rateLimit, validator, sanitizer,
}),
});2. Extensions for Context, Not API Structure
// ✅ Good - add context
const auth = extension({
request: (state, ctx) => ({
...ctx,
user: await getUser(ctx.userId),
}),
});
// ❌ Bad - don't add methods to t
const badExtension = extension({
functions: (t) => ({
customQuery: t.query({ /* ... */ }),
}),
});3. Make Extensions Reusable
// ✅ Good - configurable
const createCacheExtension = (options: {
client: RedisClient;
defaultTTL?: number;
}) => extension({
name: "caching",
request: (state, ctx) => ({
...ctx,
cache: {
get: (key) => options.client.get(key),
set: (key, val) => options.client.set(key, val, options.defaultTTL),
},
}),
});
// Usage
.withExtensions([createCacheExtension({ client: redis, defaultTTL: 300 })])4. Use Descriptive Extension Names
// ✅ Good - clear names
extension({ name: "logging", ... })
extension({ name: "authentication", ... })
extension({ name: "rate-limiting", ... })
// ❌ Bad - vague names
extension({ name: "ext1", ... })
extension({ name: "helper", ... })
extension({ name: "stuff", ... })See Also
- Context System - Context merging with extensions
- Queries & Mutations - Endpoints that use context
- Lifecycle Hooks - Hooks that run before/after handlers