Skip to content

Instantly share code, notes, and snippets.

@robinebers
Created October 12, 2025 15:41
Show Gist options
  • Select an option

  • Save robinebers/5a316c6206c7cf6ba44cf8537b1f8152 to your computer and use it in GitHub Desktop.

Select an option

Save robinebers/5a316c6206c7cf6ba44cf8537b1f8152 to your computer and use it in GitHub Desktop.
Must-follow patterns for Convex mutations, queries, and usage of `api.` or `internal.`
---
description: Must-follow patterns for Convex mutations, queries, and usage of `api.` or `internal.`
alwaysApply: false
---
# Convex API Reference Patterns
## Overview
To avoid TypeScript deep instantiation errors (TS2589) when working with Convex's generated API objects, we use lightweight function references created with `makeFunctionReference` instead of importing `api` or `internal` from `convex/_generated/api`.
## The Problem
When the Convex API grows large, TypeScript's type resolver hits its recursion limit when trying to resolve types from the generated `api` and `internal` objects:
```
Type instantiation is excessively deep and possibly infinite. TS2589
```
This happens when:
- Using `FunctionReturnType<typeof api.*>` in components
- Passing `api.*` references directly to hooks like `useQuery`, `useMutation`, `useAction`
- Calling Convex functions via `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction`
- Having a large number of Convex functions in your project
## The Solution
We use `makeFunctionReference` with explicit function paths to create lightweight function references that don't trigger deep type resolution.
### Directory Structure
```
convex/refs/
├── api.ts # Public API function references
├── internal.ts # Internal API function references
└── webhooks.ts # Webhook function references
```
## Usage Patterns
### 1. Client Components (React Hooks)
**❌ DON'T:**
```typescript
import { api } from '@/convex/_generated/api';
import { useQuery, useMutation, useAction } from 'convex/react';
// Causes deep instantiation errors
const courses = useQuery(api.courses.list);
const createCourse = useMutation(api.courses.create);
const generateDescription = useAction(api.lessons.generateDescription);
```
**✅ DO:**
```typescript
import { apiRefs } from '@/convex/refs/api';
import { useQuery, useMutation, useAction } from 'convex/react';
// Uses lightweight references
const courses = useQuery(apiRefs.courses.list);
const createCourse = useMutation(apiRefs.courses.create);
const generateDescription = useAction(apiRefs.lessons.generateDescription);
```
### 2. Server Components & API Routes
**❌ DON'T:**
```typescript
import { api } from '@/convex/_generated/api';
import { createAuthenticatedConvexClient } from '@/lib/convex/server';
const convex = await createAuthenticatedConvexClient();
const data = await convex.query(api.courses.getDashboardData, {});
```
**✅ DO:**
```typescript
import { apiRefs } from '@/convex/refs/api';
import { createAuthenticatedConvexClient } from '@/lib/convex/server';
const convex = await createAuthenticatedConvexClient();
const data = await convex.query(apiRefs.courses.getDashboardData, {});
```
### 3. Convex Functions (Actions calling queries/mutations)
**❌ DON'T:**
```typescript
import { internal } from "./_generated/api";
import { action } from "./_generated/server";
export const myAction = action({
handler: async (ctx, args) => {
// Causes deep instantiation errors
const user = await ctx.runQuery(internal.users.getCurrent);
await ctx.runMutation(internal.messages.create, { text: "Hello" });
},
});
```
**✅ DO:**
```typescript
import { internalRefs } from "@/convex/refs/internal";
import { action } from "./_generated/server";
export const myAction = action({
handler: async (ctx, args) => {
// Uses lightweight references
const user = await ctx.runQuery(internalRefs.users.getCurrent);
await ctx.runMutation(internalRefs.messages.create, { text: "Hello" });
},
});
```
### 4. Type Annotations
**❌ DON'T:**
```typescript
import { FunctionReturnType } from "convex/server";
import { api } from "@/convex/_generated/api";
// Causes deep instantiation errors
type CourseData = FunctionReturnType<typeof api.courses.getBySlug>;
```
**✅ DO:**
```typescript
// Define explicit types or use any with eslint-disable
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CourseData = any;
// Or use the Convex-generated Doc types
import { Doc } from "@/convex/_generated/dataModel";
type CourseData = Doc<"courses">;
```
### 5. Preloaded Queries (Next.js)
**❌ DON'T:**
```typescript
import { api } from '@/convex/_generated/api';
import { preloadQuery } from 'convex/nextjs';
const preloaded = await preloadQuery(api.courses.getBySlug, { slug });
```
**✅ DO:**
```typescript
import { apiRefs } from '@/convex/refs/api';
import { preloadQuery } from 'convex/nextjs';
const preloaded = await preloadQuery(apiRefs.courses.getBySlug, { slug });
```
## Adding New Function References
When you create a new Convex function, add it to the appropriate refs file:
### For Public Functions (`convex/refs/api.ts`)
```typescript
export const apiRefs = {
// ... existing refs
myNewModule: {
myQuery: ref("myNewModule:myQuery", "query"),
myMutation: ref("myNewModule:myMutation", "mutation"),
myAction: ref("myNewModule:myAction", "action"),
},
} as const;
```
### For Internal Functions (`convex/refs/internal.ts`)
```typescript
export const internalRefs = {
// ... existing refs
myModule: {
internalQuery: ref("model/myModule/internal:internalQuery", "query"),
internalMutation: ref("model/myModule/internal:internalMutation", "mutation"),
},
} as const;
```
## Function Path Format
Function paths follow Convex's file-based routing convention:
### Top-level modules
- File: `convex/courses.ts`
- Export: `export const getBySlug = query({...})`
- Path: `"courses:getBySlug"`
### Nested modules
- File: `convex/integrations/mux.ts`
- Export: `export const createUpload = action({...})`
- Path: `"integrations/mux:createUpload"`
### Deep nesting
- File: `convex/model/profiles/internal.ts`
- Export: `export const findByEmail = internalQuery({...})`
- Path: `"model/profiles/internal:findByEmail"`
### Special cases
- Subdirectories with index files: Use the directory name
- Nested actions: Include the full path with forward slashes
## Helper Function Pattern (PREFERRED)
Instead of calling Convex functions from other Convex functions using `ctx.run*`, prefer using **helper functions** as recommended by Convex best practices:
**❌ AVOID:**
```typescript
// convex/users.ts
export const getCurrentUser = internalQuery({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
return await ctx.db.query("users").withIndex("by_clerk_id", ...).unique();
},
});
// convex/messages.ts
export const sendMessage = mutation({
handler: async (ctx, args) => {
// Requires internal ref and separate transaction
const user = await ctx.runQuery(internalRefs.users.getCurrentUser);
await ctx.db.insert("messages", { userId: user._id, ...args });
},
});
```
**✅ PREFER:**
```typescript
// convex/helpers/users.ts (NOT registered as a Convex function)
import { QueryCtx } from "../_generated/server";
export async function getCurrentUser(ctx: QueryCtx) {
const identity = await ctx.auth.getUserIdentity();
return await ctx.db.query("users").withIndex("by_clerk_id", ...).unique();
}
// convex/messages.ts
import { getCurrentUser } from "./helpers/users";
export const sendMessage = mutation({
handler: async (ctx, args) => {
// Same transaction, no ref needed, cleaner code
const user = await getCurrentUser(ctx);
await ctx.db.insert("messages", { userId: user._id, ...args });
},
});
```
**Benefits of helper functions:**
- Execute in the same transaction
- No need for function references
- Reduced TypeScript complexity
- Better performance (no extra function calls)
- Easier to refactor
**When to use `ctx.run*` with refs:**
- Calling functions from actions (different runtime)
- Need partial rollback (catch errors in `ctx.runMutation`)
- Using Convex Components
- Scheduling functions with `ctx.scheduler`
## Common Pitfalls
### 1. Using `anyApi` without explicit paths
```typescript
// ❌ This doesn't work at runtime
const ref = anyApi as FunctionReference<"query", "public", any, any>;
// ✅ Must use makeFunctionReference with explicit path
const ref = makeFunctionReference<"query", any, any>("courses:getBySlug");
```
### 2. Wrong function path format
```typescript
// ❌ Wrong: using dot notation
makeFunctionReference("courses.getBySlug")
// ✅ Correct: using colon notation
makeFunctionReference("courses:getBySlug")
```
### 3. Incorrect nested paths
```typescript
// ❌ Wrong: missing directory separators
makeFunctionReference("integrations:mux:createUpload")
// ✅ Correct: forward slashes for directories, colon for function
makeFunctionReference("integrations/mux:createUpload")
```
### 4. Mixing function types
```typescript
// ❌ Wrong: mutation is registered as "action" in refs
export const myMutation = mutation({...});
// In refs: ref("module:myMutation", "action") // WRONG!
// ✅ Correct: type must match the registration
export const myMutation = mutation({...});
// In refs: ref("module:myMutation", "mutation") // CORRECT
```
## Debugging Tips
### Runtime Error: "API path expected to be of the form `api.moduleName.functionName`"
This means you're passing an invalid function reference. Check:
1. The function path format is correct (use colons, not dots)
2. The path matches the actual file structure
3. You're using `makeFunctionReference`, not `anyApi`
### Type Error: "Type instantiation is excessively deep"
This means you're still importing from the generated API:
1. Search for `from '@/convex/_generated/api'` or `from "./_generated/api"`
2. Replace with imports from `@/convex/refs/api` or `@/convex/refs/internal`
3. Make sure helper files don't import `api` or `internal`
### Function Not Found at Runtime
Check that:
1. The Convex function is actually exported
2. The function path in refs matches the file structure
3. The function type (query/mutation/action) is correct
4. You've run `bunx convex dev` to sync functions
## Don't Export `api` or `internal`
**❌ NEVER DO THIS:**
```typescript
// lib/convex/server.ts
import { api } from "@/convex/_generated/api";
export { api }; // DON'T re-export!
```
This causes conflicts and runtime errors because:
- The generated `api` object is a proxy
- Re-exporting it can cause circular dependencies
- It defeats the purpose of using refs
## Summary
1. **Always** use `apiRefs` from `@/convex/refs/api` for public functions
2. **Always** use `internalRefs` from `@/convex/refs/internal` for internal functions
3. **Prefer** helper functions over `ctx.run*` when possible
4. **Never** import `api` or `internal` from `_generated/api` in application code
5. **Always** use `makeFunctionReference` with explicit paths in refs files
6. **Match** function types correctly (query/mutation/action)
7. **Follow** Convex file-based routing for function paths
## Related Documentation
- [Convex Multiple Repos Pattern](https://docs.convex.dev/production/multiple-repos)
- [Convex Best Practices](https://docs.convex.dev/production/best-practices)
- [makeFunctionReference API](https://docs.convex.dev/generated-api/api)
# Convex API Reference Patterns
## Overview
This project uses a custom pattern to avoid TypeScript deep instantiation errors (TS2589) when working with Convex's generated API objects. Instead of importing `api` or `internal` from `convex/_generated/api`, we use lightweight function references created with `makeFunctionReference`.
## The Problem
When the Convex API grows large, TypeScript's type resolver hits its recursion limit when trying to resolve types from the generated `api` and `internal` objects:
```
Type instantiation is excessively deep and possibly infinite. TS2589
```
This happens when:
- Using `FunctionReturnType<typeof api.*>` in components
- Passing `api.*` references directly to hooks like `useQuery`, `useMutation`, `useAction`
- Calling Convex functions via `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction`
- Having a large number of Convex functions in your project
## The Solution
We use `makeFunctionReference` with explicit function paths to create lightweight function references that don't trigger deep type resolution.
### Directory Structure
```
convex/refs/
├── api.ts # Public API function references
├── internal.ts # Internal API function references
└── webhooks.ts # Webhook function references
```
## Usage Patterns
### 1. Client Components (React Hooks)
**❌ DON'T:**
```typescript
import { api } from '@/convex/_generated/api';
import { useQuery, useMutation, useAction } from 'convex/react';
// Causes deep instantiation errors
const courses = useQuery(api.courses.list);
const createCourse = useMutation(api.courses.create);
const generateDescription = useAction(api.lessons.generateDescription);
```
**✅ DO:**
```typescript
import { apiRefs } from '@/convex/refs/api';
import { useQuery, useMutation, useAction } from 'convex/react';
// Uses lightweight references
const courses = useQuery(apiRefs.courses.list);
const createCourse = useMutation(apiRefs.courses.create);
const generateDescription = useAction(apiRefs.lessons.generateDescription);
```
### 2. Server Components & API Routes
**❌ DON'T:**
```typescript
import { api } from '@/convex/_generated/api';
import { createAuthenticatedConvexClient } from '@/lib/convex/server';
const convex = await createAuthenticatedConvexClient();
const data = await convex.query(api.courses.getDashboardData, {});
```
**✅ DO:**
```typescript
import { apiRefs } from '@/convex/refs/api';
import { createAuthenticatedConvexClient } from '@/lib/convex/server';
const convex = await createAuthenticatedConvexClient();
const data = await convex.query(apiRefs.courses.getDashboardData, {});
```
### 3. Convex Functions (Actions calling queries/mutations)
**❌ DON'T:**
```typescript
import { internal } from "./_generated/api";
import { action } from "./_generated/server";
export const myAction = action({
handler: async (ctx, args) => {
// Causes deep instantiation errors
const user = await ctx.runQuery(internal.users.getCurrent);
await ctx.runMutation(internal.messages.create, { text: "Hello" });
},
});
```
**✅ DO:**
```typescript
import { internalRefs } from "@/convex/refs/internal";
import { action } from "./_generated/server";
export const myAction = action({
handler: async (ctx, args) => {
// Uses lightweight references
const user = await ctx.runQuery(internalRefs.users.getCurrent);
await ctx.runMutation(internalRefs.messages.create, { text: "Hello" });
},
});
```
### 4. Type Annotations
**❌ DON'T:**
```typescript
import { FunctionReturnType } from "convex/server";
import { api } from "@/convex/_generated/api";
// Causes deep instantiation errors
type CourseData = FunctionReturnType<typeof api.courses.getBySlug>;
```
**✅ DO:**
```typescript
// Define explicit types or use any with eslint-disable
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CourseData = any;
// Or use the Convex-generated Doc types
import { Doc } from "@/convex/_generated/dataModel";
type CourseData = Doc<"courses">;
```
### 5. Preloaded Queries (Next.js)
**❌ DON'T:**
```typescript
import { api } from '@/convex/_generated/api';
import { preloadQuery } from 'convex/nextjs';
const preloaded = await preloadQuery(api.courses.getBySlug, { slug });
```
**✅ DO:**
```typescript
import { apiRefs } from '@/convex/refs/api';
import { preloadQuery } from 'convex/nextjs';
const preloaded = await preloadQuery(apiRefs.courses.getBySlug, { slug });
```
## Adding New Function References
When you create a new Convex function, add it to the appropriate refs file:
### For Public Functions (`convex/refs/api.ts`)
```typescript
export const apiRefs = {
// ... existing refs
myNewModule: {
myQuery: ref("myNewModule:myQuery", "query"),
myMutation: ref("myNewModule:myMutation", "mutation"),
myAction: ref("myNewModule:myAction", "action"),
},
} as const;
```
### For Internal Functions (`convex/refs/internal.ts`)
```typescript
export const internalRefs = {
// ... existing refs
myModule: {
internalQuery: ref("model/myModule/internal:internalQuery", "query"),
internalMutation: ref("model/myModule/internal:internalMutation", "mutation"),
},
} as const;
```
## Function Path Format
Function paths follow Convex's file-based routing convention:
### Top-level modules
- File: `convex/courses.ts`
- Export: `export const getBySlug = query({...})`
- Path: `"courses:getBySlug"`
### Nested modules
- File: `convex/integrations/mux.ts`
- Export: `export const createUpload = action({...})`
- Path: `"integrations/mux:createUpload"`
### Deep nesting
- File: `convex/model/profiles/internal.ts`
- Export: `export const findByEmail = internalQuery({...})`
- Path: `"model/profiles/internal:findByEmail"`
### Special cases
- Subdirectories with index files: Use the directory name
- Nested actions: Include the full path with forward slashes
## Helper Function Pattern (PREFERRED)
Instead of calling Convex functions from other Convex functions using `ctx.run*`, prefer using **helper functions** as recommended by Convex best practices:
**❌ AVOID:**
```typescript
// convex/users.ts
export const getCurrentUser = internalQuery({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
return await ctx.db.query("users").withIndex("by_clerk_id", ...).unique();
},
});
// convex/messages.ts
export const sendMessage = mutation({
handler: async (ctx, args) => {
// Requires internal ref and separate transaction
const user = await ctx.runQuery(internalRefs.users.getCurrentUser);
await ctx.db.insert("messages", { userId: user._id, ...args });
},
});
```
**✅ PREFER:**
```typescript
// convex/helpers/users.ts (NOT registered as a Convex function)
import { QueryCtx } from "../_generated/server";
export async function getCurrentUser(ctx: QueryCtx) {
const identity = await ctx.auth.getUserIdentity();
return await ctx.db.query("users").withIndex("by_clerk_id", ...).unique();
}
// convex/messages.ts
import { getCurrentUser } from "./helpers/users";
export const sendMessage = mutation({
handler: async (ctx, args) => {
// Same transaction, no ref needed, cleaner code
const user = await getCurrentUser(ctx);
await ctx.db.insert("messages", { userId: user._id, ...args });
},
});
```
**Benefits of helper functions:**
- Execute in the same transaction
- No need for function references
- Reduced TypeScript complexity
- Better performance (no extra function calls)
- Easier to refactor
**When to use `ctx.run*` with refs:**
- Calling functions from actions (different runtime)
- Need partial rollback (catch errors in `ctx.runMutation`)
- Using Convex Components
- Scheduling functions with `ctx.scheduler`
## Common Pitfalls
### 1. Using `anyApi` without explicit paths
```typescript
// ❌ This doesn't work at runtime
const ref = anyApi as FunctionReference<"query", "public", any, any>;
// ✅ Must use makeFunctionReference with explicit path
const ref = makeFunctionReference<"query", any, any>("courses:getBySlug");
```
### 2. Wrong function path format
```typescript
// ❌ Wrong: using dot notation
makeFunctionReference("courses.getBySlug")
// ✅ Correct: using colon notation
makeFunctionReference("courses:getBySlug")
```
### 3. Incorrect nested paths
```typescript
// ❌ Wrong: missing directory separators
makeFunctionReference("integrations:mux:createUpload")
// ✅ Correct: forward slashes for directories, colon for function
makeFunctionReference("integrations/mux:createUpload")
```
### 4. Mixing function types
```typescript
// ❌ Wrong: mutation is registered as "action" in refs
export const myMutation = mutation({...});
// In refs: ref("module:myMutation", "action") // WRONG!
// ✅ Correct: type must match the registration
export const myMutation = mutation({...});
// In refs: ref("module:myMutation", "mutation") // CORRECT
```
## Debugging Tips
### Runtime Error: "API path expected to be of the form `api.moduleName.functionName`"
This means you're passing an invalid function reference. Check:
1. The function path format is correct (use colons, not dots)
2. The path matches the actual file structure
3. You're using `makeFunctionReference`, not `anyApi`
### Type Error: "Type instantiation is excessively deep"
This means you're still importing from the generated API:
1. Search for `from '@/convex/_generated/api'` or `from "./_generated/api"`
2. Replace with imports from `@/convex/refs/api` or `@/convex/refs/internal`
3. Make sure helper files don't import `api` or `internal`
### Function Not Found at Runtime
Check that:
1. The Convex function is actually exported
2. The function path in refs matches the file structure
3. The function type (query/mutation/action) is correct
4. You've run `bunx convex dev` to sync functions
## Don't Export `api` or `internal`
**❌ NEVER DO THIS:**
```typescript
// lib/convex/server.ts
import { api } from "@/convex/_generated/api";
export { api }; // DON'T re-export!
```
This causes conflicts and runtime errors because:
- The generated `api` object is a proxy
- Re-exporting it can cause circular dependencies
- It defeats the purpose of using refs
## Summary
1. **Always** use `apiRefs` from `@/convex/refs/api` for public functions
2. **Always** use `internalRefs` from `@/convex/refs/internal` for internal functions
3. **Prefer** helper functions over `ctx.run*` when possible
4. **Never** import `api` or `internal` from `_generated/api` in application code
5. **Always** use `makeFunctionReference` with explicit paths in refs files
6. **Match** function types correctly (query/mutation/action)
7. **Follow** Convex file-based routing for function paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment