Framework guide
CSV and Excel importer for Remix
In Remix, keep Bootstrapware Importer on the client (a client module or interactive island). Customers upload CSV, Excel (XLSX), or TSV in the browser. Your resource route receives normalized JSON from onComplete, not a multipart spreadsheet posted to Bootstrapware.
Install
pnpm add @bootstrapware/importer
Import @bootstrapware/importer/styles.css from the client module that renders the importer (or from your root stylesheet if you prefer one global import).
Client island
Local schema example. Hosted mode swaps fields for importerId and a publishable key from env.
import { Importer } from "@bootstrapware/importer";
import "@bootstrapware/importer/styles.css";
export function ImportPanel() {
return (
<Importer
fields={[
{ key: "email", label: "Email", type: "email", required: true },
{ key: "plan", label: "Plan", type: "enum", enumValues: ["free", "pro"], required: true },
]}
duplicateKey="email"
onComplete={async (rows, meta) => {
const res = await fetch("/resources/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows, meta }),
});
if (!res.ok) throw new Error("Import failed");
}}
/>
);
}Mount <ImportPanel /> from a route that is already behind your session. Remix loaders stay for page data; the importer talks to a resource route for writes.
Resource route
Parse JSON, check the session cookie, re-validate business rules, write with your data layer, return a status the UI can toast.
// app/routes/resources.import.ts
import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const session = await requireUserSession(request);
const { rows } = await request.json();
// Re-validate emails, plans, uniqueness for session.orgId, then insert.
await createImportedRecords(session.orgId, rows);
return json({ ok: true, count: rows.length });
}Prefer an action on a resource route (or a dedicated API route) over stuffing large row arrays into a document form POST. The importer already produced clean objects; keep the handoff as JSON.
Hosted configuration and env
Expose only publishable keys to the browser. Pattern depends on your Remix setup (classic window.ENV injection from the root loader, or Vite-based Remix env). Never ship bsw_*_sec_ to the client. Details: API keys.
<Importer
importerId={ENV.IMPORTER_ID}
publishableKey={ENV.BSW_PUBLISHABLE_KEY}
onComplete={async (rows) => {
await fetch("/resources/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows }),
});
}}
/>What stays in the browser
Parsing, mapping, and validation run client-side. Hosted mode may load published field configuration and report non-content session metadata. Row payloads go only to your resource route. See Importer privacy and browser-side CSV import.
Pitfalls
- Trying to run the interactive importer only on the server
- Multipart-uploading the spreadsheet to Bootstrapware
- Skipping auth on the resource route because “the client already validated”
- Missing stylesheet import
Related: onComplete · Fields · React guide · Next.js guide · Live demo