Framework guide
CSV and Excel importer for Next.js
Use Bootstrapware Importer when your Next.js SaaS needs customers to upload CSV, Excel (XLSX), or TSV files with column mapping and validation. The widget is a Client Component. Your Route Handler receives normalized JSON rows from onComplete. Spreadsheet contents are not sent to Bootstrapware.
This guide covers App Router. The same package works in Pages Router if you keep the component on the client and avoid putting secret keys in public env vars.
Install
pnpm add @bootstrapware/importer
Import package CSS once for the tree that renders the importer. Forgetting the stylesheet is the most common “it looks broken” bug.
Local schema in a Client Component
Start with fields in code. No Bootstrapware account required. Put the widget in a file with "use client".
"use client";
import { Importer } from "@bootstrapware/importer";
import "@bootstrapware/importer/styles.css";
export function ImportCustomers() {
return (
<Importer
fields={[
{ key: "email", label: "Email", type: "email", required: true },
{ key: "name", label: "Name", type: "string", required: true },
]}
duplicateKey="email"
onComplete={async (rows, meta) => {
const res = await fetch("/api/customers/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows, meta }),
});
if (!res.ok) throw new Error("Import failed");
}}
/>
);
}Render <ImportCustomers /> from any Server or Client page. Only the importer island needs to be a Client Component.
Hosted configuration (dashboard schema)
For production, publish fields in the Bootstrapware dashboard and pass importerId plus a publishable key. Use NEXT_PUBLIC_ only for publishable values.
<Importer
importerId={process.env.NEXT_PUBLIC_IMPORTER_ID!}
publishableKey={process.env.NEXT_PUBLIC_BSW_PUBLISHABLE_KEY!}
onComplete={async (rows, meta) => {
await fetch("/api/customers/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows, meta }),
});
}}
/>Never put bsw_live_sec_ or bsw_test_sec_ in NEXT_PUBLIC_. Secret keys stay in Route Handlers or server-only modules. See API keys and environments.
Route Handler pattern
Authenticate the session, re-validate types and uniqueness against your database, then insert. Client validation is UX, not a security boundary.
// app/api/customers/import/route.ts
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const session = await getSession(); // your auth
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { rows } = (await req.json()) as { rows: Array<{ email: string; name: string }> };
// Re-check email format, uniqueness, org membership, then write with Prisma/SQL.
await insertCustomers(session.orgId, rows);
return NextResponse.json({ ok: true, count: rows.length });
}Longer patterns: validate before database insert, Postgres, Prisma.
What customers get in the UI
- Upload CSV, XLSX, or TSV
- Map spreadsheet columns to your fields (aliases, auto-detect, manual override, multi-sheet Excel picker)
- Typed validation: required, number, date, email, enum, unique
- Locales: numberLocale, dateOrder, and dir for RTL
- Preview invalid rows; export
import-errors.csv - Optional in-file duplicates via
duplicateKey
onComplete receives valid rows only, plus meta such as counts and file type. Details: onComplete.
Pitfalls specific to Next.js
- Rendering Importer in a Server Component without
"use client" - Forgetting
@bootstrapware/importer/styles.css - Putting a secret key in public env
- Uploading the raw spreadsheet to Bootstrapware (do not)
- Trusting client validation alone before database writes
Related: Quickstart · React guide · Cursor guide · Live demo