A file upload is only the beginning

The simplest bulk-import diagram is upload, parse, save. Real systems immediately complicate it: column recognition, validation, duplicate handling, authorization, partial failure, reactivation, mapping changes, and an audit trail. Hiding that work behind one long request does not remove the complexity; it removes visibility.

I model imports as a staged workflow. The uploaded file becomes an immutable input. Recognition produces a proposed mapping. Validation produces structured findings. A commit stage applies authorized changes and records what happened.

Progress should describe work

A percentage alone is rarely enough. Users need to know whether the system is reading, validating, mapping, or committing. Server-Sent Events are a good fit when the browser mainly needs one-way progress updates: they are simple to operate, reconnect naturally, and preserve ordinary HTTP semantics.

Progress events should be durable enough to reconnect to the current state. The UI can then recover from a refresh without pretending that the job restarted.

Receive named progress events in the browser
const stream = new EventSource('/api/imports/123/events');

stream.addEventListener('progress', event => {
  const update = JSON.parse(event.data) as {
    stage: 'reading' | 'validating' | 'committing';
    completed: number;
    total: number;
  };

  renderProgress(update);
});

stream.addEventListener('complete', () => stream.close());

Commit only reviewed intent

Separating validation from commit creates a useful decision point. The system can show errors and warnings, calculate the affected records, and confirm the user has permission for the complete change set before touching production data.

The best bulk tools feel calm because they make uncertainty visible early. That is a product outcome enabled by backend architecture.

All writing