Functions

Context System

Type-safe context management for your API

The Context System is the foundation of Deesse Functions. It provides type-safe dependency injection and shared state management across all your API endpoints.

What is Context?

Context is shared data that's available to all your queries and mutations:

  • User authentication (userId, session)
  • Database connections
  • External service clients
  • Configuration values
  • Logging utilities

Defining Context

Use defineContext() to create a typed context builder:

import { defineContext } from '@deessejs/functions';

// Define your context type
const { t, createAPI } = defineContext<{
  userId: string;
  database: Database;
}>({
  // Provide default values
  userId: "guest",
  database: myDatabase,
});

Default Context

Provide default values for your context:

const { t, createAPI } = defineContext({
  userId: "guest",
  locale: "en",
});

No Default Context

You can also define context without defaults:

const { t, createAPI } = defineContext<{
  userId: string;
}>();

The t Builder

The t object is used to define queries and mutations with full type safety:

const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    // ctx is typed as { userId: string; database: Database }
    console.log("Current user:", ctx.userId);

    const user = await ctx.database.find(args.id);
    return Result.success(user);
  },
});

Native Methods:

The t builder includes these methods by default:

  • t.query() - Define read operations
  • t.mutation() - Define write operations
  • t.router() - Organize endpoints

Creating APIs

Use createAPI() to activate your endpoints:

const api = createAPI({
  users: t.router({
    getUser,
    createUser,
  }),
});

Context is already defined in defineContext() - no need to pass it again!

Using Extensions

Add context-modifying extensions with withExtensions():

const { t, createAPI } = defineContext({
  userId: "guest",
}).withExtensions([
  loggingExtension,
  cacheExtension,
]);

Note: Extensions only modify context, they don't add methods to t. The query() and mutation() methods are native to t.

Context Merging

When you add extensions, their context is automatically merged:

// Extension context
interface CacheExtension {
  cache: Cache;
}

// Your context
interface MyContext {
  userId: string;
}

// Final context includes both
const { t, createAPI } = defineContext<MyContext>()
  .withExtensions([cacheExtension]);

// Context in handlers is { userId: string } & { cache: Cache }
const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    // ctx.userId - from your context
    // ctx.cache - from extension
    return Result.success(user);
  },
});

Accessing Context in Handlers

Context is always the first parameter in handlers:

const createPost = t.mutation({
  args: z.object({ title: z.string(), content: z.string() }),
  handler: async (ctx, args) => {
    // Access context
    const authorId = ctx.userId;

    // Access database from context
    const post = await ctx.database.create({
      ...args,
      authorId,
    });

    return Result.success(post);
  },
});

Type Safety

The context system provides full type safety:

const { t, createAPI } = defineContext<{
  userId: string;
  database: Database;
}>();

const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    // TypeScript knows ctx has userId and database
    console.log(ctx.userId);       // ✅ OK
    console.log(ctx.unknown);      // ❌ Type error

    return Result.success(user);
  },
});

Best Practices

1. Put Primitives in Context

// ✅ Good - simple values
const { t, createAPI } = defineContext({
  userId: "guest",
  locale: "en",
  role: "user",
});

// ❌ Bad - complex objects
const { t, createAPI } = defineContext({
  user: { id: "123", profile: {...} }, // Hard to type
});

2. Use Services, Not Data

// ✅ Good - services
defineContext({
  database: dbConnection,
  emailService: mailer,
  cache: redis,
});

// ❌ Bad - data that changes
defineContext({
  currentUser: user, // Changes per request
  posts: [],        // Request-specific data
});

3. Keep Context Minimal

// ✅ Good - only what's needed
defineContext<{
  userId: string;
  database: Database;
}>();

// ❌ Bad - everything including the kitchen sink
defineContext<{
  userId: string;
  database: Database;
  logger: Logger;
  cache: Cache;
  config: Config;
  metrics: Metrics;
  // ... 20 more things
}>();

4. Use Extensions for Cross-Cutting Concerns

// Define extension with its own context
const loggingExtension = extension({
  context: (ctx) => ({
    logger: console,
  }),
});

// Extension context is automatically merged
const { t, createAPI } = defineContext<{
  userId: string;
}>().withExtensions([loggingExtension]);

// Handlers have both userId and logger

Real-World Example

import { defineContext, Result } from '@deessejs/functions';
import { z } from 'zod';

// Define context with application dependencies
const { t, createAPI } = defineContext<{
  userId: string;
  database: Database;
  emailService: EmailService;
}>({
  userId: "guest",
  database: myDatabase,
  emailService: myMailer,
}).withExtensions([
  loggingExtension,
  cacheExtension,
]);

// Define queries and mutations
const getUser = t.query({
  cacheKey: ['users', '{id}'],
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    // Access database from context
    const user = await ctx.database.find(args.id);

    if (!user) {
      return Result.failure(new Error("User not found"));
    }

    return Result.success(user);
  },
});

const sendEmail = t.mutation({
  invalidate: [['users', '{userId}']],
  args: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
  handler: async (ctx, args) => {
    // Use emailService from context
    await ctx.emailService.send({
      from: ctx.userId,
      to: args.to,
      subject: args.subject,
      body: args.body,
    });

    return Result.success({ sent: true });
  },
});

// Create API
const api = createAPI({
  users: t.router({
    getUser,
    sendEmail,
  }),
});

See Also

On this page