REST API File Upload: Best Practices, Design Patterns & Code Examples
This guide is the reference for designing and implementing REST API file uploads correctly. It covers the protocol-level details (multipart/form-data anatomy, status codes, idempotency), the design decisions you have to make (POST vs PUT, sync vs async, response schema), and working code in cURL, Python, Node.js, Java, and PHP. If you just want a quick copy-paste example in one language, see How to Upload Files via API instead.
What Is a REST API File Upload?
A REST API file upload is an HTTP POST request that transmits binary file data from a client to a server. The file is encoded using the multipart/form-data content type, which is the standard defined in RFC 7578 for sending files over HTTP.
Unlike JSON payloads where the body is a single text block, multipart requests split the body into named parts separated by a boundary string. Each part can carry different data: one part holds the file bytes, another might hold a description or metadata field. This design allows a single request to transmit both binary and text data simultaneously.
REST APIs that accept file uploads typically return a JSON response containing a URL where the file can be accessed, a unique file identifier, and the file size. The API handles storage, CDN distribution, and URL generation, so your application only needs to make one HTTP call.
HTTP Request Anatomy
Understanding the raw HTTP request helps you debug problems when uploads fail. Here is what a file upload request looks like on the wire:
POST /v1/upload HTTP/1.1
Host: filepost.dev
X-API-Key: your_api_key_here
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 84532
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf
[binary file bytes]
------WebKitFormBoundary7MA4YWxkTrZu0gW--
The key elements are:
- Content-Type header: Must be
multipart/form-datawith aboundaryparameter. The boundary is an arbitrary string that separates each part of the body. Your HTTP library generates this automatically. - Boundary delimiters: Each part starts with
--followed by the boundary string. The final boundary ends with an additional--. - Content-Disposition: Identifies the form field name (
file) and the original filename (report.pdf). - Authentication header: Most APIs use a header like
X-API-KeyorAuthorization: Bearerfor authentication. FilePost uses theX-API-Keyheader.
The critical rule: never set the Content-Type header manually when sending multipart requests. Your HTTP library needs to generate the boundary string and embed it in both the header and the body. If you override the header, the boundary will not match and the server will reject the request with a 400 error.
REST API Design Decisions for File Uploads
Before writing any code, a REST API that accepts file uploads has to settle several design decisions. Getting these right at the start avoids painful rewrites later.
HTTP Verb: POST vs PUT
Use POST when the server generates the resource identifier. The client uploads a file, the server returns a new file_id and URL. This is the default for most file upload APIs because the client usually does not know (or care) what the canonical ID will be.
Use PUT when the client controls the resource identifier. A good example is S3: the client decides the object key and PUTs the bytes to that exact path. PUT is idempotent — repeating the request replaces the resource at the same URL. POST is not, and a retry may create a duplicate.
If you are choosing between the two for a new API, pick POST unless you have a specific reason to let clients name resources. POST is what developers expect from upload endpoints, and it matches the conventions of every major file upload provider.
Response Schema: What to Return
A well-designed upload response includes everything the client needs to store and reference the file later. At minimum:
{
"file_id": "a1b2c3d4e5f6",
"url": "https://cdn.example.com/f/a1b2c3d4e5f6.pdf",
"filename": "report.pdf",
"content_type": "application/pdf",
"size": 84210,
"created_at": "2026-04-23T10:32:14Z"
}
Two common mistakes to avoid:
- Returning only a URL. The client will eventually need to delete, list, or look up the file. Without a stable
file_id, they have to parse the URL, which breaks when you change URL structure. - Not returning size or content_type. The client had this information at upload time, but may not have stored it. Returning it saves a follow-up metadata request.
Status Codes
REST conventions for file upload responses:
- 200 OK — File uploaded and fully processed, response body contains the resource. Use this when the upload is synchronous and complete.
- 201 Created — File uploaded and a new resource was created. Include a
Locationheader pointing at the resource URL. Technically more correct than 200 for POST uploads, though 200 is widely accepted. - 202 Accepted — File was received but processing is asynchronous. Return a job ID or polling URL so the client can check status. See "Async Upload Patterns" below.
- 400 Bad Request — Malformed multipart body, missing
filefield, or invalid boundary. - 401 Unauthorized — API key is missing or invalid.
- 413 Payload Too Large — File exceeds the per-plan or per-API size limit.
- 415 Unsupported Media Type — File type is not allowed (e.g., you restrict to images and the client sent an executable).
- 429 Too Many Requests — Rate limit or upload quota exhausted. Include a
Retry-Afterheader when appropriate.
Idempotency and Upload Deduplication
File uploads are inherently not idempotent: a naive retry creates a duplicate. For any upload flow where retries can happen (mobile apps on flaky networks, workflow engines with retry policies), your API should support deduplication.
The standard pattern is an idempotency key: the client generates a UUID per logical upload and sends it in a header like Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000. The server caches the response keyed by that UUID for some window (commonly 24 hours). Retries with the same key return the cached response instead of storing the file twice.
A simpler alternative is content hashing: the client computes the file's SHA-256 before upload and sends it as a header. The server uses it as a deduplication key on its side. Stripe and most payment APIs use idempotency keys; most file storage APIs use content hashes. Both work.
Sync vs Async Upload Patterns
Simple synchronous upload works for most use cases:
POST /v1/upload (file bytes)
→ 200 OK { "file_id": "...", "url": "..." }
However, if your API does non-trivial processing on uploaded files (virus scanning, image transformations, video transcoding), a single synchronous request either ties up an HTTP worker for a long time or hits client/proxy timeouts. The async pattern returns immediately and lets the client poll for completion:
POST /v1/upload (file bytes)
→ 202 Accepted { "job_id": "...", "status_url": "/v1/uploads/jobs/abc123" }
GET /v1/uploads/jobs/abc123
→ 200 OK { "status": "processing" }
→ ... later ...
→ 200 OK { "status": "complete", "file_id": "...", "url": "..." }
For very large files, the third option is presigned URLs: the API returns a short-lived upload URL pointing directly at object storage, and the client PUTs bytes to that URL. This offloads the upload from your API servers entirely and is what S3, GCS, and Azure Blob use. The tradeoff is that you cannot inspect the bytes during upload — validation has to happen via a post-upload webhook or storage event.
URL Structure and Resource Design
Resource-oriented REST would suggest POST /v1/files (collection) for uploads and DELETE /v1/files/{id} for deletion, with the upload endpoint named for the noun rather than the verb. POST /v1/upload is also common and acceptable, but less "RESTful" in strict interpretations.
If you support multiple upload modes (direct, presigned, chunked), name the routes clearly:
POST /v1/files— direct uploadPOST /v1/files/presign— request a presigned URLPOST /v1/files/{id}/chunks— upload a chunk of a multipart uploadPOST /v1/files/{id}/complete— finalize a multipart upload
Version your API from day one (/v1/), even if v2 feels far away. The alternative is painful migrations when you inevitably need to change the response schema.
cURL Example
cURL is the simplest way to upload a file to a REST API. The -F flag automatically handles multipart encoding:
curl -X POST https://filepost.dev/v1/upload \
-H "X-API-Key: your_api_key_here" \
-F "file=@./document.pdf"
The @ prefix tells cURL to read the file from the filesystem. Without it, cURL would send the literal string as text. cURL sets Content-Type: multipart/form-data and generates the boundary automatically when you use -F.
A successful response:
{
"url": "https://cdn.filepost.dev/file/filepost/uploads/a1/a1b2c3.pdf",
"file_id": "a1b2c3d4e5f6",
"size": 84210
}
Python Example
Python's requests library is the standard for HTTP file uploads. The files parameter handles multipart encoding automatically:
import requests
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": "your_api_key_here"},
files={"file": open("document.pdf", "rb")},
timeout=30
)
data = response.json()
print(f"URL: {data['url']}")
print(f"File ID: {data['file_id']}")
print(f"Size: {data['size']} bytes")
Important details for Python uploads:
- Always open files in binary mode (
"rb"). Text mode corrupts binary data. - Do not set
Content-Typein the headers dict. Therequestslibrary generates it with the correct boundary when you use thefilesparameter. - Set a
timeoutto prevent the request from hanging indefinitely on slow connections or large files. - For production code, use a
withstatement to ensure the file handle is closed after the upload completes.
Node.js Example
Node.js 18+ includes built-in fetch and FormData, so no external packages are needed:
import { readFile } from "fs/promises";
const fileBuffer = await readFile("./document.pdf");
const file = new File([fileBuffer], "document.pdf", {
type: "application/pdf",
});
const form = new FormData();
form.append("file", file);
const response = await fetch("https://filepost.dev/v1/upload", {
method: "POST",
headers: { "X-API-Key": "your_api_key_here" },
body: form,
});
const data = await response.json();
console.log(`URL: ${data.url}`);
console.log(`File ID: ${data.file_id}`);
console.log(`Size: ${data.size} bytes`);
The fetch API detects that the body is a FormData instance and sets the Content-Type header with the boundary automatically. This example uses top-level await, which requires "type": "module" in your package.json.
Java Example
Java 11+ includes HttpClient in the standard library, but building multipart requests requires manual boundary handling since there is no built-in FormData equivalent:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileUpload {
public static void main(String[] args) throws Exception {
String apiKey = "your_api_key_here";
Path filePath = Path.of("document.pdf");
String boundary = "----FilePostBoundary" + System.currentTimeMillis();
byte[] fileBytes = Files.readAllBytes(filePath);
String mimeType = Files.probeContentType(filePath);
if (mimeType == null) mimeType = "application/octet-stream";
String bodyPrefix = "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\""
+ filePath.getFileName() + "\"\r\n"
+ "Content-Type: " + mimeType + "\r\n\r\n";
String bodySuffix = "\r\n--" + boundary + "--\r\n";
byte[] prefixBytes = bodyPrefix.getBytes();
byte[] suffixBytes = bodySuffix.getBytes();
byte[] body = new byte[prefixBytes.length + fileBytes.length + suffixBytes.length];
System.arraycopy(prefixBytes, 0, body, 0, prefixBytes.length);
System.arraycopy(fileBytes, 0, body, prefixBytes.length, fileBytes.length);
System.arraycopy(suffixBytes, 0, body, prefixBytes.length + fileBytes.length, suffixBytes.length);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://filepost.dev/v1/upload"))
.header("X-API-Key", apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
}
}
Java's HttpClient does not generate multipart boundaries for you, so you must construct the body manually. The boundary string in the Content-Type header must exactly match the delimiters in the body. For production applications, consider using Apache HttpComponents or OkHttp, which provide multipart builders that handle this automatically.
PHP Example
PHP's built-in cURL extension handles file uploads cleanly using CURLFile. This is the correct approach for PHP 5.5+ : the old @/path/to/file string syntax is deprecated and disabled by default in modern PHP.
<?php
$apiKey = 'your_api_key_here';
$filePath = '/path/to/document.pdf';
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://filepost.dev/v1/upload',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $apiKey],
CURLOPT_POSTFIELDS => ['file' => new CURLFile($filePath)],
]);
$response = curl_exec($curl);
curl_close($curl);
$data = json_decode($response, true);
echo 'URL: ' . $data['url'] . PHP_EOL;
echo 'File ID: ' . $data['file_id'] . PHP_EOL;
echo 'Size: ' . $data['size'] . ' bytes' . PHP_EOL;
Do not set the Content-Type header yourself. When you pass an array to CURLOPT_POSTFIELDS containing a CURLFile object, PHP sets the correct multipart/form-data header with the boundary automatically.
Common Errors and Troubleshooting
These are the errors developers hit most often when implementing REST API file uploads, along with how to fix each one:
413 Request Entity Too Large
The file exceeds the server's maximum upload size. Check the API documentation for size limits. FilePost allows up to 50MB on the free tier, 200MB on Starter, and 500MB on Pro. If you are behind a reverse proxy like Nginx, also check its client_max_body_size setting, as it defaults to 1MB and will reject larger uploads before they reach your API.
401 Unauthorized
The API key is missing, invalid, or expired. Verify that you are sending the correct header name. FilePost uses X-API-Key, but other APIs may use Authorization: Bearer <token>. Double-check for leading or trailing whitespace in the key value.
400 Bad Request
Usually means the multipart body is malformed. The most common cause is setting the Content-Type header manually instead of letting your HTTP library generate it. If you must set the header (as in Java), ensure the boundary string matches exactly in both the header and the body delimiters.
Request Timeout
Large file uploads on slow connections can exceed default timeout values. Increase your client timeout proportional to the file size. A reasonable formula is: base_timeout + (file_size_mb * 2) seconds. Also check if a proxy or load balancer has its own timeout that is lower than your client timeout.
Connection Reset or Broken Pipe
This typically happens when the server closes the connection before the client finishes sending the body. Common causes include exceeding the max upload size (the server rejects early) or a network interruption. Implement retry logic with exponential backoff for transient failures, but check for 413 errors first to avoid retrying permanently rejected uploads.
Best Practices
- Validate before uploading. Check file size and MIME type on the client side before sending the request. This saves bandwidth and gives users faster feedback.
- Always set a timeout. File uploads can hang indefinitely on dropped connections. Set a timeout proportional to the expected file size.
- Stream large files. For files over 100MB, use streaming uploads rather than reading the entire file into memory. Python's
requestsstreams automatically when you pass a file object. In Node.js, useReadableStreamor thenode-fetchlibrary with stream support. - Store the file ID. URLs are useful for display, but file IDs are required for management operations like deletion. Store both in your database.
- Handle retries with care. File uploads are not idempotent. A retry may create a duplicate. Implement deduplication at the application level using file hashes or client-generated upload IDs.
- Use HTTPS exclusively. File uploads often carry sensitive documents. Never send files or API keys over unencrypted HTTP.
- Do not hardcode API keys. Store keys in environment variables or a secrets manager. Never commit API keys to version control.
Upload Your First File in 30 Seconds
FilePost gives you 300 free uploads per month with a single API call. No buckets, no IAM roles, no SDK required.
Get Your Free API Key