Express CSV Logo

React Quickstart

Use this pre-built prompt to get started faster with React.

Alternatively, follow these steps to manually add ExpressCSV to a React app and relay validated rows to your backend.

  1. Install the React SDK

    npm install @expresscsv/react
  2. On your backend: create a session endpoint 🔒

    Create a small backend route that exchanges your ExpressCSV secret key for a short-lived session token. Your frontend will call this route right before opening the importer.

    For example, in a Hono backend:

    import { serve } from "@hono/node-server";
    import { Hono } from "hono";
    
    const app = new Hono();
    
    app.post("/api/expresscsv/session", async (c) => {
      // This route lives on your backend so your secret key never reaches the browser.
      const response = await fetch("https://api.expresscsv.com/v1/importer/sessions", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.YOUR_EXPRESSCSV_SECRET_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({}),
      });
    
      // Return only the short-lived token your frontend needs to open the importer.
      const { token } = await response.json();
    
      return c.json({ token });
    });
    
    serve({
      fetch: app.fetch,
      port: 4000,
    });
  3. On your frontend: define your import schema

    Start by describing the columns you want to accept. These labels and validation rules shape the importer experience.

    import { x } from "@expresscsv/react";
    
    export const productSchema = x.row({
      // Ask for the SKU in the format your catalog already uses.
      sku: x
        .string()
        .label("SKU")
        .refine(/^[A-Z]{3}-\d{4}$/, { message: "Format must be ABC-1234" }),
    
      // Require a product name so every imported row is clearly identified.
      name: x.string().min(1).label("Product Name"),
    
      // Parse prices as numbers up front so your backend receives clean data.
      price: x.number().min(0).currency("USD").label("Price"),
    
      // Map incoming category values to the controlled options your app supports.
      categories: x
        .multiselect([
          { label: "Apparel", value: "apparel" },
          { label: "Electronics", value: "electronics" },
          { label: "Home Goods", value: "home-goods" },
        ])
        .min(1)
        .label("Category"),
    });

    You can also instead define your schemas in a shared package, which you use in both your frontend and backend. For this use, @expresscsv/schema which is a lighter schema-only dependency that contains no frontend functionality.

  4. On your frontend: open the importer

    This button fetches a fresh session token, opens the importer, and sends validated rows to your backend in chunks.

    import { useExpressCSV } from "@expresscsv/react";
    import { productSchema } from "./product-schema";
    
    export function ImportProductsButton() {
      // Create the importer once for this button and point it at your schema.
      const { open, isOpen } = useExpressCSV({
        schema: productSchema,
        getSessionToken: async () => {
          // Ask your backend for a fresh session token right before the importer opens.
          const response = await fetch("/api/expresscsv/session", {
            method: "POST",
          });
          const { token } = await response.json();
          return token;
        },
        importIdentifier: "product-import",
        title: "Import products",
      });
    
      return (
        <button
          type="button"
          disabled={isOpen}
          onClick={() => {
            open({
             chunkSize: 500,
             // Open the importer when the user is ready to upload a file.
             onData: async (chunk, next) => {
               // Send each validated chunk to your backend so you can store it your way.
               const response = await fetch("/api/import-products/chunks", {
                 method: "POST",
                 headers: {
                   "Content-Type": "application/json",
                 },
                 body: JSON.stringify({
                   sessionId: chunk.sessionId,
                   chunkIdempotencyKey: chunk.chunkIdempotencyKey,
                   records: chunk.records,
                 }),
               });
    
               if (!response.ok) {
                 // Stop the import if your backend cannot accept this chunk.
                 throw new Error("Backend rejected this import chunk");
               }
    
               // Move to the next chunk only after your backend has accepted this one.
               next();
             },
             onComplete: async ({ sessionId }) => {
               // Finish the import after every chunk has been accepted.
               await fetch("/api/import-products/complete", {
                 method: "POST",
                 headers: {
                   "Content-Type": "application/json",
                 },
                 body: JSON.stringify({ sessionId }),
               });
             },
             onCancel: async ({ sessionId }) => {
               // Clean up any partial work if the user cancels.
               await fetch("/api/import-products/abort", {
                 method: "POST",
                 headers: {
                   "Content-Type": "application/json",
                 },
                 body: JSON.stringify({ sessionId }),
               });
             },
             onError: async (error, { sessionId }) => {
               console.error("Import error", error);
    
               // Clean up if something goes wrong during delivery.
               await fetch("/api/import-products/abort", {
                 method: "POST",
                 headers: {
                   "Content-Type": "application/json",
                 },
                 body: JSON.stringify({ sessionId }),
               });
             },
            });
          }}
        >
          Import Products
        </button>
      );
    }

What's Next

  • Types — define field types, validation rules, and TypeScript types
  • Styling — theme the importer to match your app
  • Delivery — relay records to your backend with onData
  • Importer SDK — full option and return value reference