plans
2026-05-04
2026 05 04 API Routes to Convex

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.ts or reservations.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

  1. Spec coverage: All 10 API routes are replaced with mutations
  2. No HTTP layer: Direct mutation calls, no httpAction needed
  3. Frontend updated: All fetch calls replaced with useMutation
  4. API routes deleted: No orphaned API route files remain

Architecture Notes

Why no HTTP actions?

For frontend-only operations, HTTP actions add unnecessary complexity:

ApproachComplexityUse When
Direct mutationLowFrontend calls Convex only
HTTP actionHigherExternal 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 dev

Frontend mutations work immediately - no endpoint testing needed since they're called from React hooks.