Ali Madooei
Ali Madooei2mo ago

Internal queries/mutations/actions vs helper functions

Hi everyone! I'm trying to understand the difference between internal queries/mutations/actions and regular helper functions. I'm going to write down my thoughts here, in a contrived example., and I hope you can help me understand it better. Thanks!
// convex/somefile.ts
import { internalQuery, query } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

// Do we have to `export` it? Or the 'query' factory will make it public irrespective?
export const myPublicQuery = query({
args: {
a: v.string(), // some argument; it will be validated
},
handler: async (ctx, args) => {

// I can call `myPrivateQuery` here
await ctx.runQuery(internal.somefile.myPrivateQuery, args);

// I can also directly call `myPrivateQuery` here like a regular function
await myPrivateQuery(ctx, args);

// So, should I use `runQuery` or call the internal query directly?

// I can call `myHelper` here as well
await myHelper(ctx, args);

// So, why when would I want a helper to be internal query/mutation/action?
// Is the difference only in the arguments validation?
},
});


// Do we need the `export` here? Even in other files it can be accessed through
// `internal.somefile.myPrivateQuery` as long as it is an `internalQuery`, right?
export const myPrivateQuery = internalQuery({
args: {
a: v.string(), // This argument will be validated
},
handler: async (ctx, args) => {
// Do something
},
});


// Doesn't have to be exported, but can be if you want to use it in other files
export const myHelper = (ctx: QueryCtx, { a }: { a: string }) => {
// Do the exact same thins as in `myPrivateQuery`
// If I want to validate the arguments, I must do it myself using e.g. Zod
};
// convex/somefile.ts
import { internalQuery, query } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

// Do we have to `export` it? Or the 'query' factory will make it public irrespective?
export const myPublicQuery = query({
args: {
a: v.string(), // some argument; it will be validated
},
handler: async (ctx, args) => {

// I can call `myPrivateQuery` here
await ctx.runQuery(internal.somefile.myPrivateQuery, args);

// I can also directly call `myPrivateQuery` here like a regular function
await myPrivateQuery(ctx, args);

// So, should I use `runQuery` or call the internal query directly?

// I can call `myHelper` here as well
await myHelper(ctx, args);

// So, why when would I want a helper to be internal query/mutation/action?
// Is the difference only in the arguments validation?
},
});


// Do we need the `export` here? Even in other files it can be accessed through
// `internal.somefile.myPrivateQuery` as long as it is an `internalQuery`, right?
export const myPrivateQuery = internalQuery({
args: {
a: v.string(), // This argument will be validated
},
handler: async (ctx, args) => {
// Do something
},
});


// Doesn't have to be exported, but can be if you want to use it in other files
export const myHelper = (ctx: QueryCtx, { a }: { a: string }) => {
// Do the exact same thins as in `myPrivateQuery`
// If I want to validate the arguments, I must do it myself using e.g. Zod
};
4 Replies
Convex Bot
Convex Bot2mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
lee
lee2mo ago
Cool good questions. I can go through them one at a time 1. Do you have to export const myPublicQuery = query(...) ? Yes, the export is necessary for Convex to register the function. 2. Should you call ctx.runQuery in a query? You can, but it's usually unnecessary. It spins up a new javascript environment and does argument validation, so there's more overhead than just calling a helper function. ctx.runQuery is mainly useful for calling code in components. https://www.convex.dev/components 3. Can you call a Convex function directly as if it were a javascript function? Technically yes, but this is discouraged and we will probably disallow it soon. Refactoring out the handler as a helper function is better, because the behavior is more explicit. One example of unexpected behavior is in this thread https://discord.com/channels/1019350475847499849/1302240820119994368/1309162182960877589 . Calling the Convex function directly does not run argument validation and doesn't create an isolated environment (which is unexpected, as evidenced by you thinking it does run argument validation) 4. Do you need to export internalQueries? Yes, this is necessary to register the function with in the Convex runtime.
Components
Independent, modular, TypeScript building blocks for your backend.
sshader
sshader2mo ago
Just to add on some general things about query, internalQuery and helper functions: - anything using the query wrapper that is exported is a public Convex function -- you can call it from any of the Convex clients (e.g. useQuery(api.my.func)) as well as from other convex functions (ctx.runQuery, and if this were a mutation or action, ctx.scheduler ) - anything using the internalQuery wrapper that is exported is an internal Convex function -- it's not callable from Convex clients, but is callable from convex functions (ctx.runQuery and ctx.scheduler for mutations + actions) - helper functions are just normal JS functions -- Convex clients don't have any way to directly call them, and neither does anything on ctx As Lee said, if you're trying to share common code, you want a helper function as opposed to ctx.runQuery (https://docs.convex.dev/production/best-practices/#use-helper-functions-to-write-shared-code). But if you're changing JS environments (like calling a query from an action), using components, or scheduling functions to run in the future, you will want an internalQuery. And then anything you want to call from your client code will be a public query
Best Practices | Convex Developer Hub
Here's a collection of our recommendations on how best to use Convex to build
Ali Madooei
Ali MadooeiOP2mo ago
Thanks Lee and sshader. This was very helpful - I appreciate it.

Did you find this page helpful?