7.5 KiB
7.5 KiB
Convex Framework Guidelines
Generic Convex best practices for building backend applications.
Function Guidelines
New Function Syntax
ALWAYS use the new function syntax for Convex functions:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const f = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
// Function body
},
});
HTTP Endpoint Syntax
HTTP endpoints are defined in convex/http.ts and require an httpAction decorator:
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/echo",
method: "POST",
handler: httpAction(async (ctx, req) => {
const body = await req.bytes();
return new Response(body, { status: 200 });
}),
});
HTTP endpoints are registered at the exact path you specify.
Validators
Array validator example:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export default mutation({
args: {
simpleArray: v.array(v.union(v.string(), v.number())),
},
handler: async (ctx, args) => {
//...
},
});
Discriminated union example:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
results: defineTable(
v.union(
v.object({
kind: v.literal("error"),
errorMessage: v.string(),
}),
v.object({
kind: v.literal("success"),
value: v.number(),
}),
),
)
});
Always use v.null() when returning null:
export const exampleQuery = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
return null;
},
});
Convex Types Reference
| Convex Type | TS/JS type | Example | Validator | Notes |
|---|---|---|---|---|
| Id | string | doc._id |
v.id(tableName) |
|
| Null | null | null |
v.null() |
Use null instead of undefined |
| Int64 | bigint | 3n |
v.int64() |
-2^63 to 2^63-1 |
| Float64 | number | 3.1 |
v.number() |
IEEE-754 double-precision |
| Boolean | boolean | true |
v.boolean() |
|
| String | string | "abc" |
v.string() |
UTF-8, <1MB |
| Bytes | ArrayBuffer | new ArrayBuffer(8) |
v.bytes() |
<1MB |
| Array | Array | [1, 3.2] |
v.array(values) |
Max 8192 values |
| Object | Object | {a: "abc"} |
v.object({property: value}) |
Max 1024 entries |
| Record | Record | {"a": "1"} |
v.record(keys, values) |
ASCII keys only |
Function Registration
- Use
internalQuery,internalMutation,internalActionfor private functions (only callable by other Convex functions) - Use
query,mutation,actionfor public API functions - You CANNOT register functions through the
apiorinternalobjects - ALWAYS include argument and return validators. Use
returns: v.null()if no return value
Function Calling
ctx.runQuery- call a query from query, mutation, or actionctx.runMutation- call a mutation from mutation or actionctx.runAction- call an action from action (only for crossing runtimes V8 to Node)- All calls take a
FunctionReference, not the function directly - Minimize calls from actions to queries/mutations (risk of race conditions)
When calling functions in the same file, add type annotation:
export const g = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
return null;
},
});
Function References
- Use
apiobject for public functions:api.example.f - Use
internalobject for private functions:internal.example.g - File-based routing:
convex/messages/access.ts->api.messages.access.h
Validator Guidelines
v.bigint()is deprecated - usev.int64()instead- Use
v.record()for record types.v.map()andv.set()are not supported
Schema Guidelines
- Define schema in
convex/schema.ts - Import schema functions from
convex/server - System fields
_creationTime(v.number()) and_id(v.id(tableName)) are automatic - Name indexes after their fields:
["field1", "field2"]->"by_field1_and_field2" - Index fields must be queried in order defined
TypeScript Guidelines
- Use
Id<'tableName'>from./_generated/dataModelfor typed IDs - Record example:
Record<Id<"users">, string> - Use
as constfor string literals in discriminated unions - Always define arrays as
const array: Array<T> = [...] - Always define records as
const record: Record<K, V> = {...} - Add
@types/nodewhen using Node.js built-in modules
Query Guidelines
- Do NOT use
filter- define an index and usewithIndexinstead - No
.delete()- use.collect()then iterate withctx.db.delete(row._id) - Use
.unique()for single document (throws if multiple match) - For async iteration, use
for await (const row of query)instead of.collect()
Ordering
- Default: ascending
_creationTime - Use
.order('asc')or.order('desc') - Indexed queries ordered by index columns
Full Text Search
const messages = await ctx.db
.query("messages")
.withSearchIndex("search_body", (q) =>
q.search("body", "hello hi").eq("channel", "#general"),
)
.take(10);
Mutation Guidelines
ctx.db.replace- fully replace document (throws if not exists)ctx.db.patch- shallow merge updates (throws if not exists)
Action Guidelines
- Add
"use node";at top of files using Node.js modules - Actions don't have database access (
ctx.dbnot available)
import { action } from "./_generated/server";
export const exampleAction = action({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
return null;
},
});
Pagination
import { paginationOptsValidator } from "convex/server";
export const listWithExtraArg = query({
args: { paginationOpts: paginationOptsValidator, author: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_author", (q) => q.eq("author", args.author))
.order("desc")
.paginate(args.paginationOpts);
},
});
paginationOpts: { numItems: number, cursor: string | null }
Returns: { page: Doc[], isDone: boolean, continueCursor: string }
Cron Jobs
- Use
crons.intervalorcrons.crononly (not hourly/daily/weekly helpers) - Pass FunctionReference, not the function directly
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.interval("job name", { hours: 2 }, internal.crons.myFunction, {});
export default crons;
File Storage
ctx.storage.getUrl()returns signed URL (null if file doesn't exist)- Query
_storagesystem table for metadata (don't use deprecatedctx.storage.getMetadata)
const metadata = await ctx.db.system.get(args.fileId);
// Returns: { _id, _creationTime, contentType?, sha256, size }
- Store items as
Blobobjects