Reliable file uploads require more than a file picker and a POST request. This guide explains how to upload files with an API using multipart forms or presigned URLs, then validate, monitor, scan, and store those files safely as your application grows.
Overview
File uploads sit at the boundary between a user’s device, your application server, and often a separate object-storage system. Each boundary introduces practical concerns: request size, network interruptions, content validation, permissions, privacy, and operational cost.
The right upload design depends mainly on file size, expected traffic, security requirements, and how much control your backend needs over the transfer. For small files and simple forms, a multipart/form-data request handled by your API is usually the clearest approach. For larger files or high-volume systems, a presigned URL lets the client upload directly to storage without sending the file through your application server. Resumable uploads can improve reliability when files are large or network conditions are inconsistent.
Regardless of the transport method, treat an upload as untrusted input. The filename, extension, browser-reported MIME type, file contents, and metadata should all be considered claims to verify rather than facts to accept.
Core framework for API file uploads
1. Choose the transfer pattern
A multipart form data file upload sends fields and binary content in one HTTP request. The client typically uses a form with enctype="multipart/form-data", or constructs a FormData object in JavaScript. The API receives the stream, validates it, and either stores it directly or forwards it to object storage.
This pattern is useful when the server must authenticate the user, associate the file with a database record, apply business rules, or return a result immediately. Its main limitation is that the application server handles the file bytes, which can increase memory, bandwidth, and processing pressure.
A presigned URL upload separates authorization from transfer. Your backend authenticates the user and requests a short-lived, narrowly scoped upload URL from the storage provider. The browser then sends the file directly to that URL. After the transfer, the client notifies your backend, or the storage system triggers a server-side event, so the application can verify and finalize the upload.
Presigned URLs are often a better fit for larger files, but they require careful coordination. The URL should be limited to the intended object key, method, content type where appropriate, and expiration window. Do not treat possession of a URL as a substitute for application-level authorization.
2. Validate before and after transfer
Client-side checks improve usability but are not security controls. Use them to display an early message when a file exceeds the stated size limit or has an unsupported extension. Repeat every important check on the server or in a trusted processing pipeline.
Useful validation layers include:
- Size: enforce a maximum before accepting or finalizing the object. Apply limits at the web server, API framework, storage policy, and application level where possible.
- Type: compare the declared MIME type and extension with the file signature or content inspection. A filename ending in
.jpgdoes not prove that the content is an image. - Name: generate a storage key rather than using a user-controlled filename as a path. Preserve the original name separately for display if needed.
- Ownership: confirm that the authenticated user is allowed to upload to the requested project, account, or record.
- Content: parse or process files in an isolated service when their format can contain active content, macros, scripts, or complex embedded objects.
For systems that accept documents from unknown or external users, add malware scanning before making a file available for download or internal use. A practical status model might be pending, scanning, available, and rejected. Avoid exposing a newly uploaded object merely because the transfer completed.
3. Make the upload observable
Users need clear feedback, especially for large files. A browser can track progress for a multipart request through an upload-capable HTTP client or an XMLHttpRequest progress event. With presigned URLs, track the direct storage request and then show a separate processing state while the backend verifies the object.
On the server, record an upload identifier, user or tenant reference, byte count, expected size when known, storage key, validation result, and timestamps. Logs should help distinguish an expired URL, a rejected type, a network interruption, a storage error, and a malware-scan failure. Do not log file contents or sensitive tokens.
Practical examples
Simple multipart upload flow
A basic browser flow can be structured as follows:
- The user selects a file and the browser checks its apparent size and type for immediate feedback.
- The client sends a
FormDatarequest to an authenticated upload endpoint. - The API enforces request and file-size limits before accepting the full body when its framework supports streaming or early rejection.
- The backend generates a server-controlled storage key, validates the content, and stores the file outside the application’s executable or public web directory.
- The application creates a database record with a processing status and returns an upload identifier.
For example, the request may contain a file field named document and ordinary fields such as projectId. The server should derive the authenticated account from the session or access token rather than trusting an account identifier supplied by the browser.
Presigned URL flow for larger files
- The client asks the API to start an upload and provides metadata such as the intended size and a permitted content category.
- The API authenticates the request, checks quotas and permissions, creates an upload record, and returns a short-lived presigned URL or multipart-upload instructions.
- The browser uploads directly to storage and displays progress.
- The client calls a completion endpoint, or the backend receives a storage event.
- The backend checks that the object exists, verifies its size and content, scans it when required, and changes its status to available only after successful processing.
If users regularly send large files, consider resumable file uploads. These divide a file into parts and allow failed parts to be retried without restarting the entire transfer. The implementation must track upload identifiers, part numbers, checksums where supported, expiration, and cleanup of abandoned uploads. Pair this with a clear retry policy so a temporary failure does not create duplicate records or indefinite storage costs.
For broader file-sharing workflows, separate upload infrastructure from sharing permissions. An upload should not automatically create a public link. When recipients need access, issue authenticated or expiring download permissions and review the approach described in secure file sharing and encryption best practices.
Common mistakes
- Trusting the extension or MIME header: both can be changed by the client. Validate content using server-side inspection appropriate to the file type.
- Using the original filename as a path: this can cause collisions, unsafe paths, or unexpected characters. Generate opaque keys and store display names separately.
- Loading every file into memory: stream uploads and set explicit limits to reduce resource exhaustion risk.
- Making objects public by default: keep storage private and authorize downloads through your application or controlled, expiring links.
- Skipping cleanup: remove incomplete multipart uploads, rejected objects, expired temporary files, and records that never completed.
- Confusing upload completion with processing completion: a transferred file may still need validation, conversion, thumbnailing, indexing, or malware scanning.
- Returning detailed internal errors: show users a useful status without exposing storage credentials, paths, scanner details, or infrastructure information.
Also test the unhappy paths: a connection dropped halfway through, a file just over the limit, a valid image with a misleading extension, an expired presigned URL, two completion requests, and a user who loses permission while an upload is in progress.
When to revisit your upload design
Review the implementation whenever the primary transfer method changes, your storage provider introduces a new upload mechanism, or your application begins handling larger files or more concurrent users. Revisit it after adding new file types, accepting uploads from untrusted parties, introducing mobile clients, or connecting a new malware-scanning or document-processing service.
As a practical maintenance checklist, confirm that limits are documented in one place, presigned URLs are scoped and short-lived, rejected files are removed, incomplete uploads are cleaned up, download permissions remain private by default, and monitoring distinguishes client errors from infrastructure failures. Run a small end-to-end test after changes to the browser client, API gateway, storage policy, or processing worker.
If your needs have outgrown a basic endpoint, map the workflow explicitly: authorize, initiate, transfer, verify, scan, publish, and clean up. That sequence gives developers a dependable foundation for multipart forms, presigned URL uploads, and resumable transfers without treating file delivery as an afterthought. For related operational guidance, see this guide to sending large files securely online.