Learn-skills.dev convex-patterns
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.
install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/aaronvanston/skills-convex/convex-patterns" ~/.claude/skills/neversight-learn-skills-dev-convex-patterns && rm -rf "$T"
manifest:
data/skills-md/aaronvanston/skills-convex/convex-patterns/SKILL.mdsource content
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:
// convex/model/users.ts 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) { throw new ConvexError({ code: "NOT_FOUND", message: "User not found" }); } return user; } export async function getById(ctx: QueryCtx, userId: Id<"users">): Promise<Doc<"users"> | null> { return await ctx.db.get("users", userId); }
// convex/users.ts (thin wrapper) 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
// convex/schema.ts 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("medium"), v.literal("high")), }) .index("by_user", ["userId"]) .index("by_user_and_status", ["userId", "status"]), });
TypeScript Types
// Context types import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server"; // Document and ID types import { Doc, Id } from "./_generated/dataModel"; type User = Doc<"users">; type UserId = Id<"users">; // Infer types from validators import { Infer, v } from "convex/values"; const priorityValidator = v.union(v.literal("low"), v.literal("medium"), v.literal("high")); type Priority = Infer<typeof priorityValidator>; // "low" | "medium" | "high" // Without system fields (for inserts) import { WithoutSystemFields } from "convex/server"; type NewUser = WithoutSystemFields<Doc<"users">>; // Client-side return types import { FunctionReturnType } from "convex/server"; type UserData = FunctionReturnType<typeof api.users.get>;
Validator Types Reference
| Validator | TypeScript | Example |
|---|---|---|
| | |
| | |
| | |
| | |
| | Document reference |
| | |
| | |
| | Optional field |
| | Multiple types |
| | Exact value |
Reusable Validators
// convex/validators.ts 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")), });