DeesseJS RPC

Context

Define shared state for all procedures

Context is the shared state passed to all procedure handlers. It allows you to inject dependencies like database connections, authentication info, or any other service your procedures need.

Static Context

The static context approach initializes dependencies at module load time. This works well when your services are already initialized before the server starts:

// src/api.ts
import { defineContext } from '@deessejs/server'

const db = await createDatabase() // Connection created here

const { t, createAPI } = defineContext({
  context: {
    db, // This exact object is the context
    auth: authService,
  },
})

When to use:

  • Your dependencies are already initialized (singletons, pools)
  • You want simpler, more readable code
  • The context doesn't need any setup logic

You pass an object directly. No function call happens - the reference is stored as-is.

Lazy Context (Factory)

The lazy context (factory) approach delays initialization until createAPI() is called. This is best for delaying expensive initialization:

// src/api.ts
import { defineContext } from '@deessejs/server'

const { t, createAPI } = defineContext({
  createContext: () => ({
    db: myDatabase, // Connection created when API is created
    auth: authService,
  }),
})

When to use:

  • You want to delay expensive initialization until the API starts
  • You need setup logic (async initialization, validation)
  • You want to keep initialization logic near the API definition

You pass a factory function that runs once when createAPI() is invoked. This delays resource creation until the API is actually initialized.

Initialization Timing

To understand when context is created, consider this example showing the timing of initialization:

// src/api.ts
const { createAPI } = defineContext({
  createContext: () => {
    console.log('Context created!') // This runs when createAPI() is called
    return { db: myDatabase }
  },
})

console.log('defineContext done') // Runs first

// Context not created yet!
export const api = createAPI({ router: appRouter })
// NOW "Context created!" prints

The createContext factory runs when you call createAPI(), not when you call defineContext(). This is useful if you want initialization to happen at the point where the server starts, rather than at module load time.

Type Safety

The context type is fully inferred from your definitions. This example shows how TypeScript automatically types your context:

// src/api.ts
import { defineContext } from '@deessejs/server'
import { ok } from '@deessejs/fp'

const { t, createAPI } = defineContext({
  context: {
    db: myDatabase,
    user: null as { id: string; name: string } | null,
  },
})

// Handler has full type inference
const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    // ctx is fully typed: { db: Database; user: {...} | null }
    const user = await ctx.db.users.find(args.id)
    return ok(user)
  },
})

Best Practices

1. Keep Context Minimal

To keep context minimal, only include what's actually needed in handlers. This example demonstrates the contrast:

// Don't - unnecessary dependencies
context: {
  logger: myLogger,
  cache: redisCache,
  db: database,
  config: appConfig,
  utils: helperFunctions,
}

// Do - only what handlers need
context: {
  db: database,
  auth: authService,
}

2. Use Lazy Creation for Expensive Resources

To avoid expensive initialization at module load time, use lazy creation. This example shows the difference:

// Don't - connection at definition time
const db = await createDatabase()
context: {
  db,
}

// Do - lazy factory
createContext: () => ({
  db: myDatabase,
})

3. Handle Missing Auth Gracefully

To handle missing authentication gracefully, initialize context with null user and let middleware enrich it. This example shows the pattern:

createContext: () => {
  // Setup context with null user initially
  // Auth enrichment should be handled via middleware or headers
  return {
    user: null,
  }
}

API Reference

Prop

Type

Common Patterns

Database Connection

To add a database connection to your context, define it in the factory function. This example shows the pattern:

// src/api.ts
import { defineContext } from '@deessejs/server'

const { t, createAPI } = defineContext({
  createContext: () => ({
    db: myDatabase,
  }),
})

Multiple Services

To add multiple services to your context, include each one in the context object. This example shows how to wire up several services:

// src/api.ts
import { defineContext } from '@deessejs/server'

const { t, createAPI } = defineContext({
  context: {
    db: mysqlDatabase,
    cache: redisCache,
    mailer: emailService,
  },
})

Next Steps

  • Queries - Define read operations
  • Mutations - Define write operations
  • Routers - Organize procedures into namespaces

On this page