Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Context Management - Zagora
Skip to content

Context Management

Pass shared dependencies to handlers

Context allows you to inject dependencies like databases, loggers, or configuration into your handlers.

Setting Initial Context

Use .context() to define initial context:

import { z } from 'zod';
import { zagora } from 'zagora';
 
const getUser = zagora()
  .context({ 
    db: myDatabase,
    logger: console
  })
  .input(z.string())
  .handler(({ context }, userId) => {
    context.logger.log('Fetching user:', userId);
    return context.db.findUser(userId);
  })
  .callable();

Runtime Context Override

Override or extend context at call site via .callable():

const getUser = zagora()
  .context({ db: productionDb })
  .input(z.string())
  .handler(({ context }, userId) => context.db.findUser(userId))
  .callable();
 
// Use production db (from initial context)
getUser('123');
 
// Override with test db
const testGetUser = getUser.callable({ 
  context: { db: testDb } 
});
testGetUser('123');

Context Merging

Runtime context is deep-merged with initial context:

const proc = zagora()
  .context({ 
    db: myDb,
    config: { timeout: 5000 }
  })
  .handler(({ context }) => {
    console.log(context.db);      // myDb
    console.log(context.config);  // { timeout: 5000 }
    console.log(context.extra);   // 'value'
  })
  .callable({ 
    context: { extra: 'value' } 
  });
 
// Handler sees merged context:
// { db: myDb, config: { timeout: 5000 }, extra: 'value' }

TypeScript Inference

Context is fully typed:

const proc = zagora()
  .context({ db: myDatabase })
  .handler(({ context }) => {
    context.db;      // typeof myDatabase
    context.other;   // TypeScript error!
  })
  .callable();

With runtime context:

const proc = zagora()
  .context({ db: myDatabase })
  .handler(({ context }) => {
    context.db;      // typeof myDatabase
    context.logger;  // Logger (from runtime)
  })
  .callable({ context: { logger: myLogger } });

Pattern: Dependency Injection

Use context for clean dependency injection:

// Define procedure with dependencies
const createUser = zagora()
  .input(z.object({ name: z.string(), email: z.string() }))
  .handler(async ({ context }, input) => {
    const user = await context.userService.create(input);
    await context.emailService.sendWelcome(user.email);
    context.analytics.track('user_created', { userId: user.id });
    return user;
  });
 
// Production
const prodCreateUser = createUser.callable({
  context: {
    userService: new UserService(prodDb),
    emailService: new EmailService(sendgrid),
    analytics: new Analytics(mixpanel)
  }
});
 
// Testing
const testCreateUser = createUser.callable({
  context: {
    userService: mockUserService,
    emailService: mockEmailService,
    analytics: mockAnalytics
  }
});

Pattern: Request Context

Pass request-specific data:

const getResource = zagora()
  .input(z.string())
  .handler(({ context }, resourceId) => {
    // Check permissions using request context
    if (!context.user.canAccess(resourceId)) {
      throw new Error('Forbidden');
    }
    return context.db.find(resourceId);
  });
 
// In request handler
app.get('/resource/:id', (req, res) => {
  const proc = getResource.callable({
    context: { 
      user: req.user,
      db: req.db
    }
  });
  
  const result = proc(req.params.id);
  // ...
});

Context Without Input

Context works without input:

const getCurrentUser = zagora()
  .context({ auth: authService })
  .handler(({ context }) => {
    return context.auth.getCurrentUser();
  })
  .callable();

Next Steps