{"id":"convex-best-practices","name":"convex-best-practices","summary":"関数の構成、クエリパターン、検証、TypeScriptの使用、エラー処理、そして凸設計の禅(Zen of Convex)設計哲学を網羅した本番対応のConvexアプリ構築ガイドライン","body":"# Convex Best Practices\n\nBuild production-ready Convex applications by following established patterns for function organization, query optimization, validation, TypeScript usage, and error handling.\n\n## Code Quality\n\nAll patterns in this skill comply with `@convex-dev/eslint-plugin`. Install it for build-time validation:\n\n```bash\nnpm i @convex-dev/eslint-plugin --save-dev\n```\n\n```js\n// eslint.config.js\nimport { defineConfig } from \"eslint/config\";\nimport convexPlugin from \"@convex-dev/eslint-plugin\";\n\nexport default defineConfig([\n  ...convexPlugin.configs.recommended,\n]);\n```\n\nThe plugin enforces four rules:\n\n| Rule                                | What it enforces                  |\n| ----------------------------------- | --------------------------------- |\n| `no-old-registered-function-syntax` | Object syntax with `handler`      |\n| `require-argument-validators`       | `args: {}` on all functions       |\n| `explicit-table-ids`                | Table name in db operations       |\n| `import-wrong-runtime`              | No Node imports in Convex runtime |\n\nDocs: https://docs.convex.dev/eslint\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https://docs.convex.dev/understanding/best-practices/\n- Error Handling: https://docs.convex.dev/functions/error-handling\n- Write Conflicts: https://docs.convex.dev/error#1\n- For broader context: https://docs.convex.dev/llms.txt\n\n## Instructions\n\n### The Zen of Convex\n\n1. **Convex manages the hard parts** - Let Convex handle caching, real-time sync, and consistency\n2. **Functions are the API** - Design your functions as your application's interface\n3. **Schema is truth** - Define your data model explicitly in schema.ts\n4. **TypeScript everywhere** - Leverage end-to-end type safety\n5. **Queries are reactive** - Think in terms of subscriptions, not requests\n\n### Function Organization\n\nOrganize your Convex functions by domain:\n\n```typescript\n// convex/users.ts - User-related functions\nimport { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\nexport const get = query({\n  args: { userId: v.id(\"users\") },\n  returns: v.union(\n    v.object({\n      _id: v.id(\"users\"),\n      _creationTime: v.number(),\n      name: v.string(),\n      email: v.string(),\n    }),\n    v.null(),\n  ),\n  handler: async (ctx, args) => {\n    return await ctx.db.get(\"users\", args.userId);\n  },\n});\n```\n\n### Argument and Return Validation\n\nAlways define validators for arguments AND return types:\n\n```typescript\nexport const createTask = mutation({\n  args: {\n    title: v.string(),\n    description: v.optional(v.string()),\n    priority: v.union(v.literal(\"low\"), v.literal(\"medium\"), v.literal(\"high\")),\n  },\n  returns: v.id(\"tasks\"),\n  handler: async (ctx, args) => {\n    return await ctx.db.insert(\"tasks\", {\n      title: args.title,\n      description: args.description,\n      priority: args.priority,\n      completed: false,\n      createdAt: Date.now(),\n    });\n  },\n});\n```\n\n### Query Patterns\n\nUse indexes instead of filters for efficient queries:\n\n```typescript\n// Schema with index\nexport default defineSchema({\n  tasks: defineTable({\n    userId: v.id(\"users\"),\n    status: v.string(),\n    createdAt: v.number(),\n  })\n    .index(\"by_user\", [\"userId\"])\n    .index(\"by_user_and_status\", [\"userId\", \"status\"]),\n});\n\n// Query using index\nexport const getTasksByUser = query({\n  args: { userId: v.id(\"users\") },\n  returns: v.array(\n    v.object({\n      _id: v.id(\"tasks\"),\n      _creationTime: v.number(),\n      userId: v.id(\"users\"),\n      status: v.string(),\n      createdAt: v.number(),\n    }),\n  ),\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query(\"tasks\")\n      .withIndex(\"by_user\", (q) => q.eq(\"userId\", args.userId))\n      .order(\"desc\")\n      .collect();\n  },\n});\n```\n\n### Error Handling\n\nUse ConvexError for user-facing errors:\n\n```typescript\nimport { ConvexError } from \"convex/values\";\n\nexport const updateTask = mutation({\n  args: {\n    taskId: v.id(\"tasks\"),\n    title: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const task = await ctx.db.get(\"tasks\", args.taskId);\n\n    if (!task) {\n      throw new ConvexError({\n        code: \"NOT_FOUND\",\n        message: \"Task not found\",\n      });\n    }\n\n    await ctx.db.patch(\"tasks\", args.taskId, { title: args.title });\n    return null;\n  },\n});\n```\n\n### Avoiding Write Conflicts (Optimistic Concurrency Control)\n\nConvex uses OCC. Follow these patterns to minimize conflicts:\n\n```typescript\n// GOOD: Make mutations idempotent\nexport const completeTask = mutation({\n  args: { taskId: v.id(\"tasks\") },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const task = await ctx.db.get(\"tasks\", args.taskId);\n\n    // Early return if already complete (idempotent)\n    if (!task || task.status === \"completed\") {\n      return null;\n    }\n\n    await ctx.db.patch(\"tasks\", args.taskId, {\n      status: \"completed\",\n      completedAt: Date.now(),\n    });\n    return null;\n  },\n});\n\n// GOOD: Patch directly without reading first when possible\nexport const updateNote = mutation({\n  args: { id: v.id(\"notes\"), content: v.string() },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    // Patch directly - ctx.db.patch throws if document doesn't exist\n    await ctx.db.patch(\"notes\", args.id, { content: args.content });\n    return null;\n  },\n});\n\n// GOOD: Use Promise.all for parallel independent updates\nexport const reorderItems = mutation({\n  args: { itemIds: v.array(v.id(\"items\")) },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const updates = args.itemIds.map((id, index) =>\n      ctx.db.patch(\"items\", id, { order: index }),\n    );\n    await Promise.all(updates);\n    return null;\n  },\n});\n```\n\n### TypeScript Best Practices\n\n```typescript\nimport { Id, Doc } from \"./_generated/dataModel\";\n\n// Use Id type for document references\ntype UserId = Id<\"users\">;\n\n// Use Doc type for full documents\ntype User = Doc<\"users\">;\n\n// Define Record types properly\nconst userScores: Record<Id<\"users\">, number> = {};\n```\n\n### Internal vs Public Functions\n\n```typescript\n// Public function - exposed to clients\nexport const getUser = query({\n  args: { userId: v.id(\"users\") },\n  returns: v.union(\n    v.null(),\n    v.object({\n      /* ... */\n    }),\n  ),\n  handler: async (ctx, args) => {\n    // ...\n  },\n});\n\n// Internal function - only callable from other Convex functions\nexport const _updateUserStats = internalMutation({\n  args: { userId: v.id(\"users\") },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    // ...\n  },\n});\n```\n\n## Examples\n\n### Complete CRUD Pattern\n\n```typescript\n// convex/tasks.ts\nimport { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { ConvexError } from \"convex/values\";\n\nconst taskValidator = v.object({\n  _id: v.id(\"tasks\"),\n  _creationTime: v.number(),\n  title: v.string(),\n  completed: v.boolean(),\n  userId: v.id(\"users\"),\n});\n\nexport const list = query({\n  args: { userId: v.id(\"users\") },\n  returns: v.array(taskValidator),\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query(\"tasks\")\n      .withIndex(\"by_user\", (q) => q.eq(\"userId\", args.userId))\n      .collect();\n  },\n});\n\nexport const create = mutation({\n  args: {\n    title: v.string(),\n    userId: v.id(\"users\"),\n  },\n  returns: v.id(\"tasks\"),\n  handler: async (ctx, args) => {\n    return await ctx.db.insert(\"tasks\", {\n      title: args.title,\n      completed: false,\n      userId: args.userId,\n    });\n  },\n});\n\nexport const update = mutation({\n  args: {\n    taskId: v.id(\"tasks\"),\n    title: v.optional(v.string()),\n    completed: v.optional(v.boolean()),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const { taskId, ...updates } = args;\n\n    // Remove undefined values\n    const cleanUpdates = Object.fromEntries(\n      Object.entries(updates).filter(([_, v]) => v !== undefined),\n    );\n\n    if (Object.keys(cleanUpdates).length > 0) {\n      await ctx.db.patch(\"tasks\", taskId, cleanUpdates);\n    }\n    return null;\n  },\n});\n\nexport const remove = mutation({\n  args: { taskId: v.id(\"tasks\") },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    await ctx.db.delete(\"tasks\", args.taskId);\n    return null;\n  },\n});\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Always define return validators for functions\n- Use indexes for all queries that filter data\n- Make mutations idempotent to handle retries gracefully\n- Use ConvexError for user-facing error messages\n- Organize functions by domain (users.ts, tasks.ts, etc.)\n- Use internal functions for sensitive operations\n- Leverage TypeScript's Id and Doc types\n\n## Common Pitfalls\n\n1. **Using filter instead of withIndex** - Always define indexes and use withIndex\n2. **Missing return validators** - Always specify the returns field\n3. **Non-idempotent mutations** - Check current state before updating\n4. **Reading before patching unnecessarily** - Patch directly when possible\n5. **Not handling null returns** - Document IDs might not exist\n\n## References\n\n- Convex Documentation: https://docs.convex.dev/\n- Convex LLMs.txt: https://docs.convex.dev/llms.txt\n- Best Practices: https://docs.convex.dev/understanding/best-practices/\n- Error Handling: https://docs.convex.dev/functions/error-handling\n- Write Conflicts: https://docs.convex.dev/error#1","author":"@waynesutton","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/waynesutton/convexskills/tree/main/skills/convex-best-practices","license":"Apache-2.0","category":"writing","lang":"en","tokens":2336,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"agents/openai.yaml","size":91,"sha256":"bb57e6929f0916464111ae4a5e0a2ec5d301653b5a3b818a81dc2c0a7deb21c2"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["docs.convex.dev"]}}