Back to blog

September 6, 2026 · NBForms Team

File Upload Form Backend: 7 Mistakes That Lose Submitted Files

Adding a file field to a form backend looks identical to adding a text field — until quota, encoding, or naming assumptions quietly drop the attachment. Seven specific mistakes and their fixes.

A file field looks like any other field in a form builder — drag it in, give it a name, done. It isn't like any other field once a real submission arrives. Text fields have nowhere to go wrong: they're a string, they get stored, that's the whole job. A file field involves a storage quota, a transport encoding that plain URL-encoded forms don't use, and a permanent-vs-recoverable distinction that doesn't exist anywhere else in a form backend. The mistakes below all come from treating a file field as a slightly bigger text field.

Rows of colorful ring binders organized on wooden shelves in an office Photo by Viktor Talashuk on Unsplash

Mistake: Shipping a resume or attachment field on the free plan

Why it happens: The form builder doesn't stop anyone from dragging a file or files field onto a form regardless of plan. It renders in the live preview, generates a working <input type="file"> snippet, and looks exactly as functional as every other field type.

The fix: The free plan carries no file storage allocation at all — none, not a small free allowance. A file field needs at least one storage credit pack (500 MB for 5 credits, see pricing) bought before the form goes live, or every submission with an attachment will be silently affected the moment it arrives. Text-only forms are unaffected either way; this only matters the instant a form has a file or files field on it.

Mistake: Assuming an over-quota submission just gets rejected

Why it happens: Plenty of APIs return a 402 or 413 and stop there, so it's reasonable to expect a blocked upload to look like an error the visitor (and you) would notice immediately.

The fix: That's not what happens here. The visitor's request still succeeds — they see your configured success message or redirect exactly as if nothing were wrong. What actually happens is the submission gets created with both its field data and its files hidden, filed into a separate Locked view in the dashboard rather than the main inbox, and set to auto-delete 90 days later. Nothing about the visitor's experience signals the problem, which means a form can quietly lose every attachment it receives for weeks before anyone checks the Locked tab and notices.

Mistake: Expecting file-type filtering to happen on NBForms' side

Why it happens: Some form services advertise blocking specific file types or scanning uploads, so it's a fair assumption that any form backend handling files does some filtering by default.

The fix: There's no accept-list or MIME-type filter to configure here — whatever a visitor's browser lets them attach gets accepted. If a form should only take PDFs and images, add accept=".pdf,.jpg,.jpeg,.png" to the <input type="file"> yourself:

<input type="file" name="resume" accept=".pdf,.doc,.docx" required />

That only narrows what the browser's file picker shows — a determined visitor can still rename a file's extension before selecting it, so treat it as guidance for a well-behaved browser, not a security boundary.

Mistake: Building the multipart request by hand for a fetch-based form

Why it happens: A form posting with fetch() instead of a plain HTML submit is common enough (see the Next.js and React patterns) that it's tempting to reuse the same approach — serialize the fields into a JSON body — once a file field gets added.

The fix: JSON has no way to carry binary file bytes, so a form with any file or files field needs to submit as multipart/form-data specifically. The simplest fix is passing the <form> element itself into a FormData constructor, which builds the correct multipart body automatically — including the files:

async function handleSubmit(e) {
  e.preventDefault()
  const res = await fetch('https://api.nbforms.com', {
    method: 'POST',
    body: new FormData(e.target), // reads every field, including <input type="file">
  })
}

A plain HTML <form action="https://api.nbforms.com" method="POST" enctype="multipart/form-data"> with no JavaScript at all handles this the same way without any of the above — the encoding is only something to think about once fetch is doing the submitting.

Mistake: Only using file for a field meant to take more than one attachment

Why it happens: "File" and "Files" look like a labeling nuance in the builder, not two functionally different inputs, so it's easy to drop in the singular version out of habit and add multiple to it manually later.

The fix: They're separate field types for a reason — file renders a single-attachment input, files renders one with the multiple attribute already set:

<!-- one attachment -->
<input type="file" name="resume" required />

<!-- any number of attachments -->
<input type="file" name="portfolio_samples" multiple />

Pick whichever matches what the field is actually for — a resume upload is realistically one file, a portfolio or supporting-documents field usually isn't.

Mistake: Expecting deleted files to refund the storage credit that paid for them

Why it happens: The monthly submission quota resets every month, so it's a reasonable but wrong extension to assume storage works the same way — use less, get room back automatically.

The fix: Storage credits are consumed permanently the moment they're allocated, not metered against ongoing usage. Deleting old files frees up the space those files occupied, which lowers how much of the allocation is currently used, but the credits spent to buy that allocation don't return to the balance. Budget a storage purchase as a one-time cost for a fixed amount of room, not a rental that gets cheaper by tidying up afterward.

Mistake: Parsing the uploaded file's URL to recover its original filename

Why it happens: Some storage services expose the exact filename a visitor uploaded as part of the file's public URL, so it's a fair guess this works the same way everywhere.

The fix: It doesn't here — every uploaded file gets renamed to a random string (keeping only its original extension) once it's moved into permanent storage, and that's the name in the public URL. The original filename a visitor saw on their end is preserved separately, as metadata alongside the file entry in the submissions dashboard and the notification email — read it from there rather than trying to reconstruct it from the download link.

What this adds up to

None of these are edge cases that show up rarely — a file field either has a storage credit behind it or it doesn't, either submits as multipart or it doesn't, either uses file or files correctly or it doesn't. Getting the seven above right up front is the difference between a file upload field that works the first time and one that silently drops attachments until someone goes looking for them in a tab most people never open.

For forms that lean on file uploads specifically — job applications with a resume field, real estate inquiries with a pre-approval letter — the field itself takes one line in the framework snippets for whatever's rendering the form. The mechanics above are what decide whether the file behind that line actually arrives.

Frequently asked questions

Does the free plan support file uploads at all?

No. The free plan ships with zero file storage allocated, so a file field works in the builder but any submission that actually attaches a file needs at least one storage credit pack purchased first.

What happens to a submission if I run out of storage mid-month?

It isn't rejected — the visitor still sees your success message or redirect. The submission itself lands in a separate Locked view with its field data and files hidden, and is purged automatically after 90 days unless more storage is bought.

Can I restrict uploads to specific file types, like PDFs only?

Not on the server side — there's no accept-list to configure in the dashboard. Add the restriction yourself with the HTML `accept` attribute on the input, which limits what the browser's file picker shows.

If I delete old uploaded files, do I get the storage credits back?

No. Storage credits are spent permanently the moment they're allocated. Deleting files frees up the space to use again, but doesn't refund the credit.

Do I need to change my form's encoding to add a file field?

Only if you're submitting with fetch() and building the body yourself. A plain HTML `<form>` handles the multipart encoding automatically the moment it contains an `<input type="file">` — nothing to configure by hand.