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 operationst.mutation()- Define write operationst.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 loggerReal-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
- Queries & Mutations - Defining endpoints with context
- Extensions - Creating context-modifying extensions
- Validation - Input validation with Zod