GuidesTutorial

How to Add CSV Import to Your Next.js App

The importer is a browser component, so the App Router needs one specific thing from you. Here is that, plus the route handler that receives the validated rows.

The short answer

To add CSV import to a Next.js app, install @csvbox/react and render CSVBoxButton inside a component marked "use client", since the importer requires browser APIs. Store the licence key in NEXT_PUBLIC_CSVBOX_KEY, and create a route handler at app/api/imports/route.js to receive the validated rows your dashboard destination posts.

Key takeaways

  • The App Router needs "use client" on the importer component — this is the one Next.js-specific requirement.
  • The licence key must be NEXT_PUBLIC_ prefixed or it will be undefined in the browser.
  • Rows arrive at a route handler on your server, not in the React callback.
  • Verify the webhook signature in the route handler. It is a public endpoint receiving customer data.
On this page
  1. The short answer
  2. The one thing the App Router needs
  3. Step 1: Install and configure
  4. Step 2: The client component
  5. Step 3: Receive the rows
  6. Step 4: Deployment notes
  7. Frequently asked questions

The one thing the App Router needs#

Components in the App Router are Server Components by default. The importer mounts an iframe, attaches event listeners, and manages a modal — all browser work — so it cannot run on the server.

The fix is one line at the top of the file. Miss it and you get a build-time error about hooks or browser globals in a Server Component, which is the single most common way this integration fails on Next.js.

Step 1: Install and configure#

Shell
npm install @csvbox/react

Add your sheet licence key to .env.local. The NEXT_PUBLIC_ prefix is mandatory — without it Next.js keeps the variable server-side and the key arrives as undefined in the browser.

Shell
NEXT_PUBLIC_CSVBOX_KEY=your_sheet_license_key
CSVBOX_WEBHOOK_SECRET=your_webhook_secret
.env.local

The webhook secret has no NEXT_PUBLIC_ prefix on purpose. It must stay server-side — it is what proves an incoming request actually came from CSVbox.

Step 2: The client component#

JSX
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { CSVBoxButton } from '@csvbox/react'

export default function ImportCustomers({ user }) {
  const [status, setStatus] = useState('idle')
  const router = useRouter()

  return (
    <>
      <CSVBoxButton
        licenseKey={process.env.NEXT_PUBLIC_CSVBOX_KEY}
        user={{ user_id: user.id, team_id: user.teamId }}
        onSubmit={() => setStatus('importing')}
        onImport={(result) => {
          setStatus(result ? 'done' : 'error')
          // Re-run server components so the new rows appear.
          if (result) router.refresh()
        }}
        onClose={() => setStatus('idle')}
      >
        Import customers
      </CSVBoxButton>

      {status === 'importing' && <p>Importing…</p>}
      {status === 'error' && <p>Something went wrong. Please try again.</p>}
    </>
  )
}
app/components/ImportCustomers.jsx

The router.refresh() call is the Next.js-specific detail worth noting. Rows land on your server out of band, so your Server Components are holding stale data. refresh() re-runs them without a full page reload, and the new rows appear.

You can now render this from any Server Component page — importing a client component into a server one is fine, it is the other direction that is not.

JSX
import ImportCustomers from '../components/ImportCustomers'
import { getUser, getCustomers } from '@/lib/data'

export default async function CustomersPage() {
  const user = await getUser()
  const customers = await getCustomers(user.teamId)

  return (
    <main>
      <header>
        <h1>Customers</h1>
        <ImportCustomers user={user} />
      </header>
      <CustomerTable rows={customers} />
    </main>
  )
}
app/customers/page.jsx — a Server Component

Step 3: Receive the rows#

In the CSVbox dashboard, set the destination to a webhook pointing at your deployed route handler. This is where validated rows actually arrive.

JavaScript
import crypto from 'node:crypto'
import { NextResponse } from 'next/server'
import { insertCustomers } from '@/lib/db'

function verifySignature(rawBody, signature) {
  const expected = crypto
    .createHmac('sha256', process.env.CSVBOX_WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex')

  const a = Buffer.from(expected)
  const b = Buffer.from(signature || '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

export async function POST(request) {
  // Read the raw body first — parsing and re-serialising would change
  // the bytes the signature was computed over.
  const rawBody = await request.text()
  const signature = request.headers.get('x-csvbox-signature')

  if (!verifySignature(rawBody, signature)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const payload = JSON.parse(rawBody)
  const { rows, user } = payload

  await insertCustomers(user.team_id, rows)

  return NextResponse.json({ received: rows.length })
}
app/api/imports/route.js

Two more things worth handling here: make the write idempotent so a retried delivery does not duplicate rows, and return quickly. Webhook senders retry on timeout, so if the insert is slow, acknowledge first and process in a queue.

Step 4: Deployment notes#

  • Set env vars in your host, not just `.env.local`. On Vercel, NEXT_PUBLIC_CSVBOX_KEY must be present at *build* time, since it is inlined into the client bundle. Adding it after a deploy requires a rebuild.
  • Point staging and production at different sheets. Different licence keys per environment stops test imports landing in production tables.
  • Your webhook endpoint must be publicly reachable. For local development, tunnel it — ngrok http 3000 — and set the dashboard destination to the tunnel URL while you test.
  • Watch the function timeout. Large imports arriving in one payload can exceed a serverless function limit. If you expect big files, configure batch delivery in the dashboard so rows arrive in chunks.

For the framework-agnostic version of this, see the React guide.

Frequently asked questions

Why does the CSVbox importer fail to build in Next.js?

Almost always a missing "use client" directive. The importer uses browser APIs and cannot run as a Server Component, which is the App Router default. Add "use client" as the first line of the file that renders CSVBoxButton, and it resolves.

Should the licence key use NEXT_PUBLIC_?

Yes. Without the prefix Next.js keeps the variable server-side and it arrives as undefined in the browser. The key is safe to expose — it identifies which sheet to open and grants no write access to your systems.

Does this work with the Pages Router?

Yes, and with less setup, since the Pages Router has no Server Components and needs no "use client" directive. Use an API route at pages/api/imports.js instead of a route handler; the signature verification logic is identical.

How do I refresh the page after an import completes?

Call router.refresh() from next/navigation inside the onImport callback. This re-runs your Server Components and fetches fresh data without a full page reload. Without it the import succeeds but the user still sees the old list and assumes it failed.

How do I test the webhook locally?

Expose your dev server with a tunnel such as ngrok, then set the dashboard destination to the tunnel URL. Localhost is not reachable from CSVbox servers, so imports will appear to succeed while your route handler never fires.

What happens if my API route times out on a large import?

The delivery is retried, which can duplicate rows if your handler is not idempotent. For large files, enable batch delivery so rows arrive in chunks, acknowledge the request quickly, and do the heavy write in a background job or queue.

Topicsnextjstutorialintegration

Stop building CSV importers.

Ship ours in 15 minutes. Free forever on the Sandbox plan.

No credit cardEmbed in minutesSecure by default