Functions

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

On this page