Migrate API Routes to Convex Mutations
Approach: Frontend calls Convex mutations directly via
useMutation. No HTTP action layer needed for frontend-only operations.
Goal: Replace fetch("/api/X") calls in the frontend with useMutation(api.X) calls. The existing internal mutations already have the business logic.
Tech Stack: Convex mutations, React hooks
File Structure
apps/backend/convex/functions/
├── crm.ts # MODIFY: add public mutations
└── notifications.ts # MODIFY: add public mutations
apps/frontend/app/api/crm/ # DELETE ALL
apps/frontend/app/api/notifications/ # DELETE ALL
apps/frontend/app/api/payment/vnpay/ # DELETE (if only used by frontend)Task 1: Migrate CRM Mutations (6 routes)
Files:
- Modify:
apps/backend/convex/functions/crm_sync_internal.ts→ add public mutations - Delete:
apps/frontend/app/api/crm/(all 6 routes)
The existing internal mutations (upsertZohoContactInternal, createZohoDealInternal, createZohoInvoiceInternal) have the Zoho logic. We just need to expose them as public mutations.
- Step 1: Read existing crm_sync_internal.ts
cat apps/backend/convex/functions/crm_sync_internal.ts- Step 2: Add public wrapper mutations
// apps/backend/convex/functions/crm.ts
// Public mutations that wrap the internal ones
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
// Re-export internal mutations as public
export const createContact = mutation({
args: {
firstName: v.string(),
lastName: v.string(),
email: v.string(),
phone: v.optional(v.string()),
},
handler: async (ctx, args) => {
return await ctx.runMutation(
internal.crm_sync.upsertZohoContactInternal,
args,
);
},
});
export const createDeal = mutation({
args: {
contactId: v.string(),
dealName: v.string(),
amount: v.optional(v.number()),
stage: v.optional(v.string()),
},
handler: async (ctx, args) => {
return await ctx.runMutation(
internal.crm_sync.createZohoDealInternal,
args,
);
},
});
export const createInvoice = mutation({
args: {
contactId: v.string(),
items: v.array(
v.object({
itemName: v.string(),
quantity: v.number(),
price: v.number(),
}),
),
},
handler: async (ctx, args) => {
return await ctx.runMutation(
internal.crm_sync.createZohoInvoiceInternal,
args,
);
},
});
export const markInvoicePaid = mutation({
args: {
invoiceId: v.string(),
paymentDate: v.string(),
},
handler: async (ctx, args) => {
return await ctx.runMutation(
internal.crm_sync.markInvoicePaidInternal,
args,
);
},
});
export const searchContact = mutation({
args: {
email: v.string(),
},
handler: async (ctx, args) => {
return await ctx.runMutation(internal.crm_sync.searchContactInternal, args);
},
});
export const updateContact = mutation({
args: {
contactId: v.string(),
data: v.object({}),
},
handler: async (ctx, args) => {
return await ctx.runMutation(internal.crm_sync.updateContactInternal, args);
},
});- Step 3: Delete API routes
rm -rf apps/frontend/app/api/crm- Step 4: Commit
git add apps/backend/convex/functions/crm.ts
git rm -r apps/frontend/app/api/crm
git commit -m "refactor: migrate CRM API routes to Convex mutations"Task 2: Migrate Notification Mutations (3 routes)
Files:
-
Modify:
apps/backend/convex/functions/notifications_internal.ts→ add public mutations -
Delete:
apps/frontend/app/api/notifications/(3 routes) -
Step 1: Read existing notifications_internal.ts
cat apps/backend/convex/functions/notifications_internal.ts- Step 2: Add public wrapper mutations
// apps/backend/convex/functions/notifications.ts
// Public mutations that wrap the internal ones
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const sendConfirmationEmail = mutation({
args: {
to: v.string(),
customerName: v.string(),
showName: v.string(),
date: v.string(),
time: v.string(),
quantity: v.number(),
totalAmount: v.number(),
},
handler: async (ctx, args) => {
return await ctx.runMutation(
internal.notifications.sendConfirmationEmailInternal,
args,
);
},
});
export const sendEmail = mutation({
args: {
to: v.string(),
subject: v.string(),
html: v.string(),
},
handler: async (ctx, args) => {
return await ctx.runMutation(
internal.notifications.sendEmailNotificationInternal,
args,
);
},
});
export const sendWhatsApp = mutation({
args: {
to: v.string(),
templateName: v.string(),
components: v.optional(v.array(v.object({}))),
},
handler: async (ctx, args) => {
return await ctx.runMutation(
internal.notifications.sendWhatsAppNotificationInternal,
args,
);
},
});- Step 3: Delete API routes
rm -rf apps/frontend/app/api/notifications- Step 4: Commit
git add apps/backend/convex/functions/notifications.ts
git rm -r apps/frontend/app/api/notifications
git commit -m "refactor: migrate notification API routes to Convex mutations"Task 3: Migrate Payment Confirmation Mutation (1 route)
Files:
-
Modify:
apps/backend/convex/functions/notifications_internal.tsorreservations.ts -
Delete:
apps/frontend/app/api/notifications/on-payment-confirmed/route.ts -
Step 1: Create public payment confirmation mutation
// In reservations.ts or a new file
export const confirmPaymentAndNotify = mutation({
args: {
reservationId: v.id("reservations"),
paymentId: v.string(),
paymentGateway: v.union(v.literal("VNPAY"), v.literal("ONEPAY")),
amount: v.number(),
},
handler: async (ctx, args) => {
// 1. Update reservation status
await ctx.runMutation(internal.reservations.confirmPaymentInternal, {
reservationId: args.reservationId,
paymentId: args.paymentId,
paymentGateway: args.paymentGateway,
amount: args.amount,
});
// 2. Get reservation details for notifications
const reservation = await ctx.db.get("reservations", args.reservationId);
if (!reservation) throw new Error("Reservation not found");
// 3. Send confirmation email
await ctx.runMutation(
internal.notifications.sendConfirmationEmailInternal,
{
to: reservation.customerEmail,
customerName: reservation.customerName,
// ... other fields from reservation
},
);
// 4. Send WhatsApp if applicable
if (reservation.customerPhone) {
await ctx.runMutation(
internal.notifications.sendWhatsAppNotificationInternal,
{
to: reservation.customerPhone,
templateName: "booking_confirmation",
// ...
},
);
}
return { success: true };
},
});- Step 2: Delete API route
rm -rf apps/frontend/app/api/notifications/on-payment-confirmed- Step 3: Commit
git add apps/backend/convex/functions/reservations.ts
git rm -r apps/frontend/app/api/notifications/on-payment-confirmed
git commit -m "refactor: migrate payment confirmation to Convex mutation"Task 4: Update Frontend to Use Mutations
Files to update (search for fetch calls):
grep -r "fetch.*api/crm" apps/frontend --include="*.ts" --include="*.tsx"
grep -r "fetch.*api/notifications" apps/frontend --include="*.ts" --include="*.tsx"- Step 1: Find all fetch calls to these APIs
grep -rn "fetch.*api/crm" apps/frontend/
grep -rn "fetch.*api/notifications" apps/frontend/- Step 2: Replace each with useMutation
Example - before:
const response = await fetch("/api/crm/create-contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ firstName, lastName, email }),
});
const data = await response.json();Example - after:
const createContact = useMutation(api.crm.createContact);
const data = await createContact({ firstName, lastName, email });- Step 3: Commit each frontend change separately
Task 5: Verify No Remaining API Route Calls
- Step 1: Search for remaining fetch calls to deleted routes
grep -rn "fetch.*api/crm\|fetch.*api/notifications" apps/frontend/ --include="*.ts" --include="*.tsx"Expected: No matches
- Step 2: Commit cleanup
git add -A
git commit -m "chore: remove unused API route files"Self-Review Checklist
- Spec coverage: All 10 API routes are replaced with mutations
- No HTTP layer: Direct mutation calls, no httpAction needed
- Frontend updated: All fetch calls replaced with useMutation
- API routes deleted: No orphaned API route files remain
Architecture Notes
Why no HTTP actions?
For frontend-only operations, HTTP actions add unnecessary complexity:
| Approach | Complexity | Use When |
|---|---|---|
| Direct mutation | Low | Frontend calls Convex only |
| HTTP action | Higher | External callers (webhooks, payment gateways) |
Since all 10 routes are called only by the frontend, direct mutations are the right choice.
When to use HTTP actions later
If you add:
- VNPay/OnePay callbacks from payment gateways
- Webhooks from Zoho, Resend, etc.
- Third-party API integrations that require HTTP endpoints
Then add HTTP actions for those specific cases.
Testing
npx convex devFrontend mutations work immediately - no endpoint testing needed since they're called from React hooks.