Why Dromo?

CSV import for Next.js, App Router and Pages Router

Import CSV and Excel files in Next.js without SSR headaches. Works with App Router and Pages Router, client-side parsing, AI column mapping, real-time validation.

View docs
"use client";

import dynamic from "next/dynamic";

const DromoUploader = dynamic(
  () => import("dromo-uploader-react"),
  { ssr: false }
);

Add a CSV importer to Next.js in 10 minutes

Next.js projects use the same React package as any other React app; the extra work is deciding where the client boundary sits. Install first.

pnpm add dromo-uploader-react

The importer touches the DOM, so it belongs in a Client Component. In the App Router that means a file with the "use client" directive at the top. Everything above it in the tree can stay a Server Component.

// app/(dashboard)/orders/ImportOrders.tsx
"use client";

import { useRouter } from "next/navigation";
import DromoUploader, { type IDeveloperField } from "dromo-uploader-react";

const fields: IDeveloperField[] = [
  { label: "Order ID", key: "order_id", type: "string", validators: [{ validate: "required" }, { validate: "unique" }] },
  { label: "Customer email", key: "customer_email", type: "email", validators: [{ validate: "required" }] },
  { label: "Total", key: "total", type: "number" },
  {
    label: "Status",
    key: "status",
    type: "select",
    selectOptions: [
      { label: "Pending", value: "pending" },
      { label: "Shipped", value: "shipped" },
      { label: "Refunded", value: "refunded" },
    ],
  },
];

export default function ImportOrders({ workspaceId }: { workspaceId: string }) {
  const router = useRouter();

  return (
    <DromoUploader
      licenseKey={process.env.NEXT_PUBLIC_DROMO_KEY!}
      fields={fields}
      settings={{ importIdentifier: "orders", developmentMode: process.env.NODE_ENV !== "production" }}
      user={{ id: workspaceId }}
      onResults={async (rows) => {
        await fetch("/api/orders/import", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ workspaceId, rows }),
        });
        router.refresh();
      }}
    >
      Import orders
    </DromoUploader>
  );
}

Receive the rows in a Route Handler. Because validation already ran in the browser, the handler's job is persistence and authorization, not parsing.

// app/api/orders/import/route.ts
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";

export async function POST(req: Request) {
  const session = await auth();
  if (!session) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const { workspaceId, rows } = await req.json();
  await db.order.createMany({ data: rows.map((r: any) => ({ ...r, workspaceId })) });
  return NextResponse.json({ inserted: rows.length });
}

On the Pages Router, drop the directive and load the component with next/dynamic and ssr: false. Both approaches are covered below.

How it fits the Next.js model

App Router projects draw a line between server and client, and the importer sits firmly on the client side of it. Put the "use client" file as low in the tree as you can: the page and layout stay server-rendered, fetch the tenant's schema settings on the server, and pass plain props into the client component. Functions cannot cross that boundary, so build the fields array inside the client file or pass serializable data and construct it there.

Pages Router projects have no directive, so use dynamic import to keep the module out of the server bundle entirely:

import dynamic from "next/dynamic";

const ImportOrders = dynamic(() => import("@/components/ImportOrders"), { ssr: false });

Results can go to a Route Handler, an API route, or a Server Action. A Server Action is the shortest path if the row count is modest; for very large imports, consider sending the rows to a Route Handler that enqueues a job rather than holding a serverless function open.

Next.js gotchas Dromo handles for you

window is not defined. Any library that reads window or document at import time throws during server rendering. Writing your own importer means auditing every dependency for this. The Dromo package is safe to import in a client file and does nothing until the component mounts.

Hydration mismatches. A file picker that renders differently on the server and the client produces a hydration warning and, occasionally, a broken tree. The trigger button renders identically in both passes, and the modal is created only after mount.

Request body limits. Serverless functions on most hosts cap the request body in the low megabytes, so posting a raw 80 MB spreadsheet to an API route fails before your code runs. Dromo parses and validates in the browser and delivers JSON rows, which you can batch. For files that should never transit your servers at all, Bring Your Own Storage writes results directly from the browser to your S3, GCS, or Azure bucket.

Edge runtime restrictions. Middleware and Edge Route Handlers lack the Node APIs most parsing libraries assume. Since parsing happens client-side, your handler receives JSON and works on either runtime.

Environment variables. A license key read as process.env.DROMO_KEY is undefined in the browser. Prefix it NEXT_PUBLIC_ and treat it as a publishable key, which is how the package expects it.

Dromo vs building it with PapaParse

Build it yourself Dromo
Column mapping A dropdown per column and a manual header-to-field table Suggested matches from the user's headers, remembered on the next import
Validation Bespoke checks scattered across the client and the Route Handler Declarative validators plus custom hooks that can query your API mid-import
Error correction UI Return an error list and ask the user to re-export from Excel Spreadsheet-style review step with errors highlighted and fixable inline
Large files Chunked uploads, worker threads, and body-size workarounds Browser-side processing; export an annotated workbook to finish offline
Internationalization next-intl plus strings for every state of the wizard Over thirty languages included
Ongoing maintenance Each customer's odd export lands on your sprint Adjust the schema, usually without a deploy

Someone parsing a clean CSV that their own system generated should use PapaParse in a client component and be done in an afternoon. The calculus changes once files arrive from outside: the parsing is five percent of the job, and the mapping and correction experience is the other ninety-five. That is what you are buying.

Ship it to your users

There is no separate tier for removing Dromo branding. Every plan includes white labeling, so the colors, typeface, and wording match the rest of your Next.js app from the first deploy.

Localization ships with the widget: more than thirty languages are supported out of the box, and the language can follow the user's locale you already resolve in your layout.

Private Mode keeps the file, the parsed rows, and the corrections inside the browser, with nothing retained by Dromo. Teams selling into regulated buyers turn it on with a settings change and keep the same Client Component and the same onResults handler.

What Our Customers Say

G2 High Performer Award 2025 badge for Dromo
4.9 out of 5 stars
5 out of 5 stars
Ryan Chan logo
Ryan Chan
CEO at Upkeep.com
E&Y Entrepreneur of the Year finalist
"We have now rolled out Dromo across our entire customer onboarding experience, and it has made our lives so much easier. Most importantly, our customers love the new experience!"

FAQ

Frequently asked questions

  • Yes. Mark the wrapper component with "use client" and render it like any other client component. Pages Router works with a standard dynamic import.
  • No. The importer runs entirely in the browser. Load it with a dynamic import and ssr: false, or inside a client component, and SSR is unaffected.
  • To your onResults callback as clean JSON, to a webhook, or directly to your own S3, GCS, or Azure bucket with Bring Your Own Storage.
  • Yes. Private Mode runs the whole import in the user's browser, so file contents never reach Dromo.

Ready to Ship a Better Data Import Experience?

AI-powered mapping. Real-time validation. SOC 2 certified. Deploys in minutes.