How to Add CSV Import to an AI-Built App

CSV import looks like a two-hour feature and turns into a two-week one. The work is not parsing the file, it is deciding what happens on row 4,312 when the date is in the wrong format.

Steve Jefferson
Steve Jefferson
Developer Advocate
25 August 20261 min read

How to Add CSV Import to an AI-Built App

To add CSV import to an AI-built app, build four things in this order: a column mapping step, a dry-run validation pass, a row-level error report, and an idempotent write. Parsing the file is the easy part and any AI builder will do it in one prompt. The hard part is that real customer spreadsheets are wrong in ways your test file is not, and the difference between a feature people use and a feature they abandon is entirely in the error handling.

This is a different feature from generic file upload. If you want users to attach documents or images, how to add file uploads to an AI-built app covers storage, size limits and virus scanning. CSV import is a data pipeline that happens to start with a file.

Row 4,312 is the whole feature

Here is what a real import looks like. A customer exports 5,000 contacts from their old system. The file has a UTF-8 byte order mark, semicolon separators because the export happened on a German locale, dates as 31.12.2025, three duplicate email addresses, one row where the company name contains a comma inside quotes, and 40 rows where the phone number column is empty.

A naive importer does one of two things, both bad. It rejects the whole file with "invalid format", so the customer gives up. Or it imports 4,311 rows, fails silently on the rest, and the customer discovers six weeks later that some contacts are missing.

The correct behaviour is the third option: import what is valid, report what is not, at row-level granularity, with a downloadable file of the failures the customer can fix and re-upload.

Step 1: The mapping step

Never assume the customer's column headers match your field names. Show them a mapping screen: your fields on the left, a dropdown of their columns on the right, with a best guess pre-selected.

Best-guess matching handles most cases with simple normalisation: lowercase, strip spaces and underscores, then look for exact and near matches. E-Mail Address, email_address and Email all resolve to email. Do not over-engineer this; the dropdown is the safety net.

Show three sample rows from their file underneath the mapping so they can see immediately that they have mapped Surname to first_name. This one detail eliminates most support tickets.

Step 2: Dry run before you write anything

Parse and validate the entire file, write nothing, and show a summary.

5,000 rows read
4,957 valid
   40 missing required field: email
    3 duplicate email (already in your account)

Proceed with 4,957 rows?

This costs you one extra pass over the file and it changes the feature's character completely, because the customer now knows what will happen before it happens. For large files, run this pass as a background job rather than in the request, which is where adding background jobs to an AI-built app becomes relevant.

Step 3: Row-level errors, downloadable

An error summary is not enough. Give them a CSV of the failed rows, with their original data intact and one extra column explaining what was wrong.

row

email

name

_error

812

Nadia Okafor

email is required

1104

not-an-email

Tom Brenner

email is not valid

3390

j@acme.com

Jae Lin

email already exists

The customer fixes that file and uploads it again. This turns a failed import into a two-minute task instead of a support conversation.

Step 4: Make re-uploads safe

Since you have just invited them to re-upload, the write must be idempotent. Pick a natural key, usually email or an external ID, and upsert on it. Without this, a customer who re-uploads the whole corrected file creates 4,957 duplicates and you have made things much worse than the original failure.

If your data model has no natural key, add an import_batch_id to every row you create, so a bad import can be reverted in one operation. Doing this at the database level is far easier than reconstructing it later, so decide before you ship rather than after. Choosing a database for an AI-built app covers the tradeoffs if you have not settled that yet.

The bugs that bite every first version

These are boring and they are the reason CSV import takes longer than expected. Test each one deliberately.

  • Byte order mark. Files exported from Excel on Windows often start with an invisible BOM, which turns your first header into \ufeffemail and breaks mapping. Strip it.

  • Delimiter is not a comma. European locales export with semicolons. Sniff the delimiter from the header row rather than assuming.

  • Encoding is not UTF-8. Latin-1 files are still common. Detect, and fail with a clear message rather than mangling names.

  • Line breaks inside quoted fields. An address field with a newline in it will destroy a line-by-line parser. Use a real CSV library, never split('\n').

  • Excel has eaten your data. Leading zeros in postcodes and phone numbers are gone before the file reaches you, and long numbers may arrive in scientific notation. You cannot fix this, but you can warn about it.

  • The file is 400MB. Stream it. Do not read it into memory.

Prompting an AI builder to get this right

Ask for the whole flow at once, with the failure behaviour stated explicitly, or you will get a happy-path parser.

Build a CSV import flow with four stages:

1. Upload and parse. Sniff the delimiter, strip any BOM,
   detect encoding, stream the file rather than loading it.
2. Mapping screen. My fields are: email (required), full_name,
   company, phone. Pre-select a best guess per column and show
   three sample rows under the mapping.
3. Dry run. Validate every row, write nothing, show counts of
   valid rows and each error category.
4. Commit. Upsert on email. Tag every written row with an
   import_batch_id. Produce a downloadable CSV of failed rows
   containing the original columns plus an _error column.

Partial success is required: valid rows must import even when
other rows fail. Never reject a whole file for row-level errors.

That last paragraph is the one that matters. Left unstated, most generated implementations treat any error as fatal for the whole file.

When a spreadsheet is the actual product

If your users live in spreadsheets and the import is not an onboarding step but the main way data arrives, you may be building the wrong shape of app. Turning a spreadsheet into an app with AI covers that case, where a continuous sync beats a manual import.

For the broader sequence of building and shipping, see how to build an app with AI.

FAQ

How large a CSV file should my app accept?

Stream parsing means file size is limited by time rather than memory, so the practical question is how long a customer will wait. Handle anything up to a few hundred thousand rows as a background job with progress, and set an explicit limit above that with a clear message rather than a timeout.

Should CSV import be synchronous or a background job?

Synchronous is acceptable below a few thousand rows and simpler to build. Above that, move to a background job with a progress indicator and an email when it finishes, because a browser tab that must stay open for four minutes is a support ticket waiting to happen.

How do I handle duplicate rows in a CSV import?

Decide on a natural key first, then choose one of three policies and state it in the UI: skip duplicates, update existing records, or fail the row. Update-on-match is usually what customers expect from a re-upload, which is why upsert is the safer default.

Can AI map the columns automatically instead of asking the user?

It can guess well, and it should, as a pre-selection. Do not remove the confirmation step. A silent mismatch that puts surnames into the company field is expensive to detect and painful to unwind, and the mapping screen costs the user five seconds.

What should happen if the import fails halfway through?

Either wrap the write in a transaction so nothing lands, or tag every row with a batch ID so the partial import can be reverted cleanly. The worst option is a partially imported file with no record of which rows arrived.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.