| name | convex-patterns |
| description | Code organization patterns and TypeScript best practices for Convex. Use when structuring a Convex project, writing helper functions, defining schemas, working with types like QueryCtx/MutationCtx/ActionCtx, or organizing code in a convex/model directory. Use when this capability is needed. |
| metadata | {"author":"aaronvanston"} |
Convex Patterns
Always Define Return Validators
Every function should have a returns validator:
export const getUser = query({
args: { userId: v.id("users") },
returns: v.union(
v.object({
_id: v.id("users"),
_creationTime: v.number(),
name: v.string(),
email: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get("users", args.userId);
},
});
Project Structure
convex/
├── _generated/
├── lib/ # Shared utilities
│ └── auth.ts # Auth helpers
├── model/ # Business logic
│ ├── users.ts
│ └── tasks.ts
├── schema.ts # Database schema
├── users.ts # Public API (thin wrappers)
└── tasks.ts
Helper Functions Pattern
Business logic in model/, thin wrappers in public API:
import { QueryCtx, MutationCtx } from '../_generated/server';
import { Doc, Id } from '../_generated/dataModel';
import { ConvexError } from "convex/values";
export async function getCurrentUser(ctx: QueryCtx | MutationCtx): Promise<Doc<"users">> {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError({ code: "UNAUTHENTICATED", message: "Not logged in" });
}
const user = await ctx.db
.query("users")
.withIndex("by_tokenIdentifier", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
if (!user) {
({ : , : });
}
user;
}
(): <<> | > {
ctx..(, userId);
}
import { query } from "./_generated/server";
import { v } from "convex/values";
import * as Users from "./model/users";
export const get = query({
args: { userId: v.id("users") },
returns: v.union(v.object({ _id: v.id("users"), name: v.string() }), v.null()),
handler: async (ctx, args) => Users.getById(ctx, args.userId),
});
Schema Definition
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
tokenIdentifier: v.string(),
name: v.string(),
email: v.string(),
role: v.union(v.literal("user"), v.literal("admin")),
})
.index("by_tokenIdentifier", ["tokenIdentifier"])
.index("by_email", ["email"]),
tasks: defineTable({
title: v.string(),
description: v.optional(v.string()),
userId: v.id("users"),
status: v.union(v.literal("pending"), v.literal("done")),
priority: v.union(v.literal("low"), v.literal(), v.()),
})
.(, [])
.(, [, ]),
});
TypeScript Types
import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";
import { Doc, Id } from "./_generated/dataModel";
type User = Doc<"users">;
type UserId = Id<"users">;
import { Infer, v } from "convex/values";
const priorityValidator = v.union(v.literal("low"), v.literal("medium"), v.literal("high"));
type Priority = Infer<typeof priorityValidator>;
import { WithoutSystemFields } from "convex/server";
type NewUser = WithoutSystemFields<Doc<"users">>;
import { } ;
= < api..>;
Validator Types Reference
| Validator | TypeScript | Example |
|---|
v.string() | string | "hello" |
v.number() | number | 42 |
v.boolean() | boolean | true |
v.null() | null | null |
v.id("table") | Id<"table"> | Document reference |
v.array(v) | T[] | [1, 2, 3] |
v.object({}) | { ... } | { name: "..." } |
v.optional(v) | T | undefined | Optional field |
v.union(...) | T1 | T2 | Multiple types |
v.literal(x) | "x" | Exact value |
Reusable Validators
import { v } from "convex/values";
export const userValidator = v.object({
_id: v.id("users"),
_creationTime: v.number(),
name: v.string(),
email: v.string(),
role: v.union(v.literal("user"), v.literal("admin")),
});
export const taskValidator = v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
title: v.string(),
userId: v.id("users"),
status: v.union(v.literal("pending"), v.literal("done")),
});
References
Converted and distributed by TomeVault — claim your Tome and manage your conversions.