Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

How to Build a CSV Import Feature in Your Next.js SaaS -- Let Customers Bring Their Data

August 24, 2026
nextjsdrizzle-ormsaasboilerplatezod

If a potential customer is considering switching to your SaaS, their first question is usually: "What happens to all my existing data?" The answer determines whether they sign up or close the tab.

CSV import is the fastest way to remove that objection. It lets customers pull their contacts, products, clients, or content out of a spreadsheet or a competitor tool and drop it into yours -- without manual data entry. This post shows you how to build it using the tools already in your Next.js SaaS Boilerplate.

The Four Parts of a Reliable CSV Import

A CSV import that works in a demo is different from one that works with real customer data. Real files have missing columns, inconsistent casing, extra whitespace, and rows that fail for reasons users cannot predict. A working import needs four things:

  1. A file upload endpoint that reads the raw file without timing out
  2. Row-by-row validation that returns a clear error list, not a crash
  3. A bulk database insert that handles thousands of rows efficiently
  4. A results summary the user can act on: "312 imported, 8 failed -- download error report"

Here is how to build all four.

Building the Upload Endpoint

Create a new route at app/api/import/route.ts. CSV parsing happens server-side, so this stays a Server Component -- no "use client" needed.

// app/api/import/route.ts
export async function POST(req: Request) {
  const user = await getUserFromRequest(req); // throws 401 if no valid JWT
  const form = await req.formData();
  const file = form.get("file") as File | null;
  if (!file) throw new HttpError(400, "No file provided");
  const text = await file.text();
  const rows = parseCSV(text);
  return importRows(user.id, rows);
}

Call getUserFromRequest first so unauthenticated requests fail fast. Read the file with .text() -- it handles encoding cleanly without manual buffering.

Validating Each Row With Zod

This is where most CSV importers fall apart. They validate the whole file at once, hit one bad row, and crash with a generic 500. Instead, validate row by row and collect every error:

const rowSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  plan: z.enum(["free", "pro", "enterprise"]).optional(),
});
 
function validateRows(rows: unknown[]) {
  const valid = [];
  const errors = [];
  for (const [i, row] of rows.entries()) {
    const result = rowSchema.safeParse(row);
    if (result.success) {
      valid.push(result.data);
    } else {
      errors.push({ row: i + 1, issues: result.error.flatten().fieldErrors });
    }
  }
  return { valid, errors };
}

safeParse never throws. You get a clean list of which rows failed and why -- something the user can actually fix. This follows the same Zod pattern used across feature modules in the boilerplate, so it fits the codebase without inventing new patterns.

Bulk Insert With Drizzle

Once you have validated rows, insert them in a single Drizzle call instead of looping:

await db.insert(contactTable).values(valid).onConflictDoNothing();

onConflictDoNothing means reimporting the same file twice is safe -- existing records are skipped, not duplicated. For large files, wrap this in a transaction (see the Drizzle transactions post) to make the whole import atomic.

The Import UI

The upload form is a standard React Hook Form with a file input. On submit, POST the file as FormData and show the result to the user. There are two outcomes worth surfacing:

  • "312 contacts imported successfully"
  • "8 rows skipped -- download error report"

The error report is the same CSV the user uploaded, with an extra error column explaining each failure. That format matters: customers can fix the errors in the same spreadsheet tool they used to export, then re-upload the corrected version. No support ticket required.

If you already built CSV export, the export and import share the same column schema -- keep them in sync and customers can round-trip their data without surprises.

What This Unlocks for Your Business

Once CSV import exists, your pitch to a prospect who already uses a competitor changes entirely. Instead of "export your data, then manually re-enter it here", you say: "export from the old tool, upload the file, done in under a minute."

That single workflow removes the biggest friction point in switching to a new SaaS. Customers who can migrate fast are customers who actually migrate -- and customers who complete onboarding are the ones who stick around.

The boilerplate gives you the auth layer, Drizzle schema, API route patterns, and Zod validation setup this feature needs. You are adding the CSV-specific logic on top of infrastructure that already works.

Get the Next.js SaaS Boilerplate and ship your import feature this week.