12 KiB
12 KiB
name, displayName, description, version, author, tags
| name | displayName | description | version | author | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| convex-functions | Convex Functions | Writing queries, mutations, actions, and HTTP actions with proper argument validation, error handling, internal functions, and runtime considerations | 1.0.0 | Convex |
|
Convex Functions
Master Convex functions including queries, mutations, actions, and HTTP endpoints with proper validation, error handling, and runtime considerations.
Code Quality
All examples in this skill comply with @convex-dev/eslint-plugin rules:
- Object syntax with
handlerproperty - Argument validators on all functions
- Explicit table names in database operations
See the Code Quality section in convex-best-practices for linting setup.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
- Primary: https://docs.convex.dev/functions
- Query Functions: https://docs.convex.dev/functions/query-functions
- Mutation Functions: https://docs.convex.dev/functions/mutation-functions
- Actions: https://docs.convex.dev/functions/actions
- HTTP Actions: https://docs.convex.dev/functions/http-actions
- For broader context: https://docs.convex.dev/llms.txt
Instructions
Function Types Overview
| Type | Database Access | External APIs | Caching | Use Case |
|---|---|---|---|---|
| Query | Read-only | No | Yes, reactive | Fetching data |
| Mutation | Read/Write | No | No | Modifying data |
| Action | Via runQuery/runMutation | Yes | No | External integrations |
| HTTP Action | Via runQuery/runMutation | Yes | No | Webhooks, APIs |
Queries
Queries are reactive, cached, and read-only:
import { v } from 'convex/values';
import { query } from './_generated/server';
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);
},
});
// Query with index
export const listUserTasks = query({
args: { userId: v.id('users') },
returns: v.array(
v.object({
_id: v.id('tasks'),
_creationTime: v.number(),
title: v.string(),
completed: v.boolean(),
}),
),
handler: async (ctx, args) => {
return await ctx.db
.query('tasks')
.withIndex('by_user', (q) => q.eq('userId', args.userId))
.order('desc')
.collect();
},
});
Mutations
Mutations modify the database and are transactional:
import { ConvexError, v } from 'convex/values';
import { mutation } from './_generated/server';
export const createTask = mutation({
args: {
title: v.string(),
userId: v.id('users'),
},
returns: v.id('tasks'),
handler: async (ctx, args) => {
// Validate user exists
const user = await ctx.db.get('users', args.userId);
if (!user) {
throw new ConvexError('User not found');
}
return await ctx.db.insert('tasks', {
title: args.title,
userId: args.userId,
completed: false,
createdAt: Date.now(),
});
},
});
export const deleteTask = mutation({
args: { taskId: v.id('tasks') },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete('tasks', args.taskId);
return null;
},
});
Actions
Actions can call external APIs but have no direct database access:
'use node';
import { v } from 'convex/values';
import { api, internal } from './_generated/api';
import { action } from './_generated/server';
export const sendEmail = action({
args: {
to: v.string(),
subject: v.string(),
body: v.string(),
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
// Call external API
const response = await fetch('https://api.email.com/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args),
});
return { success: response.ok };
},
});
// Action calling queries and mutations
export const processOrder = action({
args: { orderId: v.id('orders') },
returns: v.null(),
handler: async (ctx, args) => {
// Read data via query
const order = await ctx.runQuery(api.orders.get, { orderId: args.orderId });
if (!order) {
throw new Error('Order not found');
}
// Call external payment API
const paymentResult = await processPayment(order);
// Update database via mutation
await ctx.runMutation(internal.orders.updateStatus, {
orderId: args.orderId,
status: paymentResult.success ? 'paid' : 'failed',
});
return null;
},
});
HTTP Actions
HTTP actions handle webhooks and external requests:
// convex/http.ts
import { httpRouter } from 'convex/server';
import { api, internal } from './_generated/api';
import { httpAction } from './_generated/server';
const http = httpRouter();
// Webhook endpoint
http.route({
path: '/webhooks/stripe',
method: 'POST',
handler: httpAction(async (ctx, request) => {
const signature = request.headers.get('stripe-signature');
const body = await request.text();
// Verify webhook signature
if (!verifyStripeSignature(body, signature)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
// Process webhook
await ctx.runMutation(internal.payments.handleWebhook, {
eventType: event.type,
data: event.data,
});
return new Response('OK', { status: 200 });
}),
});
// API endpoint
http.route({
path: '/api/users/:userId',
method: 'GET',
handler: httpAction(async (ctx, request) => {
const url = new URL(request.url);
const userId = url.pathname.split('/').pop();
const user = await ctx.runQuery(api.users.get, {
userId: userId as Id<'users'>,
});
if (!user) {
return new Response('Not found', { status: 404 });
}
return Response.json(user);
}),
});
export default http;
Internal Functions
Use internal functions for sensitive operations:
import { v } from 'convex/values';
import {
internalAction,
internalMutation,
internalQuery,
} from './_generated/server';
// Only callable from other Convex functions
export const _updateUserCredits = internalMutation({
args: {
userId: v.id('users'),
amount: v.number(),
},
returns: v.null(),
handler: async (ctx, args) => {
const user = await ctx.db.get('users', args.userId);
if (!user) return null;
await ctx.db.patch('users', args.userId, {
credits: (user.credits || 0) + args.amount,
});
return null;
},
});
// Call internal function from action
export const purchaseCredits = action({
args: { userId: v.id('users'), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
// Process payment externally
await processPayment(args.amount);
// Update credits via internal mutation
await ctx.runMutation(internal.users._updateUserCredits, {
userId: args.userId,
amount: args.amount,
});
return null;
},
});
Scheduling Functions
Schedule functions to run later:
import { v } from 'convex/values';
import { internal } from './_generated/api';
import { internalMutation, mutation } from './_generated/server';
export const scheduleReminder = mutation({
args: {
userId: v.id('users'),
message: v.string(),
delayMs: v.number(),
},
returns: v.id('_scheduled_functions'),
handler: async (ctx, args) => {
return await ctx.scheduler.runAfter(
args.delayMs,
internal.notifications.sendReminder,
{ userId: args.userId, message: args.message },
);
},
});
export const sendReminder = internalMutation({
args: {
userId: v.id('users'),
message: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert('notifications', {
userId: args.userId,
message: args.message,
sentAt: Date.now(),
});
return null;
},
});
Examples
Complete Function File
// convex/messages.ts
import { ConvexError, v } from 'convex/values';
import { internal } from './_generated/api';
import { internalMutation, mutation, query } from './_generated/server';
const messageValidator = v.object({
_id: v.id('messages'),
_creationTime: v.number(),
channelId: v.id('channels'),
authorId: v.id('users'),
content: v.string(),
editedAt: v.optional(v.number()),
});
// Public query
export const list = query({
args: {
channelId: v.id('channels'),
limit: v.optional(v.number()),
},
returns: v.array(messageValidator),
handler: async (ctx, args) => {
const limit = args.limit ?? 50;
return await ctx.db
.query('messages')
.withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
.order('desc')
.take(limit);
},
});
// Public mutation
export const send = mutation({
args: {
channelId: v.id('channels'),
authorId: v.id('users'),
content: v.string(),
},
returns: v.id('messages'),
handler: async (ctx, args) => {
if (args.content.trim().length === 0) {
throw new ConvexError('Message cannot be empty');
}
const messageId = await ctx.db.insert('messages', {
channelId: args.channelId,
authorId: args.authorId,
content: args.content.trim(),
});
// Schedule notification
await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
channelId: args.channelId,
messageId,
});
return messageId;
},
});
// Internal mutation
export const notifySubscribers = internalMutation({
args: {
channelId: v.id('channels'),
messageId: v.id('messages'),
},
returns: v.null(),
handler: async (ctx, args) => {
// Get channel subscribers and notify them
const subscribers = await ctx.db
.query('subscriptions')
.withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
.collect();
for (const sub of subscribers) {
await ctx.db.insert('notifications', {
userId: sub.userId,
messageId: args.messageId,
read: false,
});
}
return null;
},
});
Best Practices
- Never run
npx convex deployunless explicitly instructed - Never run any git commands unless explicitly instructed
- Always define args and returns validators
- Use queries for read operations (they are cached and reactive)
- Use mutations for write operations (they are transactional)
- Use actions only when calling external APIs
- Use internal functions for sensitive operations
- Add
"use node";at the top of action files using Node.js APIs - Handle errors with ConvexError for user-facing messages
Common Pitfalls
- Using actions for database operations - Use queries/mutations instead
- Calling external APIs from queries/mutations - Use actions
- Forgetting to add "use node" - Required for Node.js APIs in actions
- Missing return validators - Always specify returns
- Not using internal functions for sensitive logic - Protect with internalMutation
References
- Convex Documentation: https://docs.convex.dev/
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
- Functions Overview: https://docs.convex.dev/functions
- Query Functions: https://docs.convex.dev/functions/query-functions
- Mutation Functions: https://docs.convex.dev/functions/mutation-functions
- Actions: https://docs.convex.dev/functions/actions