# 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: ```typescript 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: ```typescript 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: ```typescript 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: ```typescript 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: ```typescript 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`, `internalAction` for private functions (only callable by other Convex functions) - Use `query`, `mutation`, `action` for public API functions - You CANNOT register functions through the `api` or `internal` objects - 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 action - `ctx.runMutation` - call a mutation from mutation or action - `ctx.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: ```typescript 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 `api` object for public functions: `api.example.f` - Use `internal` object for private functions: `internal.example.g` - File-based routing: `convex/messages/access.ts` -> `api.messages.access.h` --- ## Validator Guidelines - `v.bigint()` is deprecated - use `v.int64()` instead - Use `v.record()` for record types. `v.map()` and `v.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/dataModel` for typed IDs - Record example: `Record, string>` - Use `as const` for string literals in discriminated unions - Always define arrays as `const array: Array = [...]` - Always define records as `const record: Record = {...}` - Add `@types/node` when using Node.js built-in modules --- ## Query Guidelines - Do NOT use `filter` - define an index and use `withIndex` instead - No `.delete()` - use `.collect()` then iterate with `ctx.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 ```typescript 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.db` not available) ```typescript import { action } from "./_generated/server"; export const exampleAction = action({ args: {}, returns: v.null(), handler: async (ctx, args) => { return null; }, }); ``` --- ## Pagination ```typescript 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.interval` or `crons.cron` only (not hourly/daily/weekly helpers) - Pass FunctionReference, not the function directly ```typescript 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 `_storage` system table for metadata (don't use deprecated `ctx.storage.getMetadata`) ```typescript const metadata = await ctx.db.system.get(args.fileId); // Returns: { _id, _creationTime, contentType?, sha256, size } ``` - Store items as `Blob` objects