Skip to content
Learni
View all tutorials
E-commerce

How to Reduce Cart Abandonment with Next.js in 2026

18 minINTERMEDIATE
Lire en français

Introduction

Cart abandonment affects an average of 70% of e-commerce sessions. A well-designed recovery system can reclaim up to 15% of those lost sales. This tutorial guides you step by step through building a complete solution with Next.js: detecting inactive carts, sending automated emails, and creating a reminder interface. We use Prisma for persistence and Resend for transactional emails.

Prerequisites

  • Node.js 20+
  • Next.js 15
  • Basic TypeScript skills
  • Resend account and PostgreSQL database
  • Prisma knowledge

Project Initialization

terminal
npx create-next-app@latest cart-recovery --yes
cd cart-recovery
npm install prisma @prisma/client resend date-fns
npx prisma init

We initialize a Next.js project and install the essential dependencies for the database and email sending.

Prisma Cart Schema

prisma/schema.prisma
model Cart {
  id        String   @id @default(cuid())
  userId    String?
  sessionId String   @unique
  items     Json
  status    String   @default("active")
  lastActive DateTime @default(now())
  createdAt DateTime @default(now())
}

The Cart model stores items, status, and last activity. The lastActive field is essential for detecting abandonments.

Cart Update API Route

app/api/cart/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

export async function POST(req: Request) {
  const { sessionId, items } = await req.json();
  const cart = await prisma.cart.upsert({
    where: { sessionId },
    update: { items, lastActive: new Date() },
    create: { sessionId, items }
  });
  return NextResponse.json(cart);
}

This route updates the cart on every change and refreshes lastActive to avoid false abandonment positives.

Abandonment Detection Function

lib/detectAbandonment.ts
import { prisma } from './prisma';
import { subHours } from 'date-fns';

export async function findAbandonedCarts() {
  const threshold = subHours(new Date(), 2);
  return prisma.cart.findMany({
    where: {
      status: 'active',
      lastActive: { lt: threshold }
    }
  });
}

This function identifies carts inactive for more than 2 hours, a configurable threshold based on your purchase cycle.

Send Reminder Email

lib/sendReminder.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendAbandonmentEmail(cart: any) {
  await resend.emails.send({
    from: 'shop@learni.dev',
    to: cart.userEmail || 'client@example.com',
    subject: 'Your cart is waiting',
    html: `<p>You left ${cart.items.length} items.</p>`
  });
  await prisma.cart.update({
    where: { id: cart.id },
    data: { status: 'reminded' }
  });
}

The email is sent via Resend and the status is updated to prevent multiple sends.

Reminder Cron Job

app/api/cron/abandonment/route.ts
import { findAbandonedCarts } from '@/lib/detectAbandonment';
import { sendAbandonmentEmail } from '@/lib/sendReminder';

export async function GET() {
  const carts = await findAbandonedCarts();
  for (const cart of carts) {
    await sendAbandonmentEmail(cart);
  }
  return Response.json({ processed: carts.length });
}

This cron endpoint can be called by Vercel Cron or an external service every hour.

Best Practices

  • Always comply with GDPR and offer clear unsubscribe options
  • Personalize emails with the exact cart contents
  • Limit reminders to 2 or 3 maximum per cart
  • Measure recovery rate with analytics events
  • Test emails across multiple email clients

Common Mistakes to Avoid

  • Forgetting to update lastActive on every user action
  • Sending emails without checking cart status
  • Using time thresholds that are too short or too long
  • Not handling email sending errors (retry logic)

Further Reading

Discover our advanced courses on modern e-commerce and marketing automation at Learni Group.