An API can receive a file in three ways. Which one it uses decides what you send, what limits apply and what can go wrong. This guide shows each with curl, then the checks worth automating.
The examples assume you have downloaded a couple of samples, for instance the 100 KB PDF and the 100 KB JSON file.
1. Multipart form data
This is what browsers send from a form, and what most upload endpoints expect. Each field, including the file, is a separate part with its own headers.
curl -sS https://api.example.test/v1/documents \
-H 'Authorization: Bearer YOUR_TOKEN' \
-F 'file=@sample-pdf-100kb.pdf;type=application/pdf' \
-F 'title=Quarterly report'The @ tells curl to read the file. The optional ;type= sets the Content-Type of that part; leave it out and curl guesses from the extension. Do not set the overall Content-Type header yourself, because curl must add the boundary parameter.
2. Raw binary body
Some APIs, and object storage services in particular, take the file as the entire request body.
curl -sS -X PUT https://api.example.test/v1/objects/report.pdf \
-H 'Content-Type: application/pdf' \
--data-binary @sample-pdf-100kb.pdfUse --data-binary, not -d. The plain -d option strips line breaks and will corrupt the file.
3. Base64 inside JSON
Some APIs accept only JSON, so files are embedded as base64 strings.
printf '{"filename":"report.pdf","content":"%s"}' \
"$(base64 < sample-pdf-100kb.pdf | tr -d '\n')" > body.json
curl -sS https://api.example.test/v1/documents \
-H 'Content-Type: application/json' \
--data-binary @body.jsonBase64 makes the payload a third larger than the file. A 100 KB file becomes about 133 KB of JSON, which is already over the 100 KB default body limit of several frameworks. Many "the file is under the limit but the API rejects it" reports are this.
From JavaScript
In a browser or in Node.js 18 and later, fetch and FormData build a multipart request:
import { readFile } from 'node:fs/promises';
const bytes = await readFile('sample-pdf-100kb.pdf');
const form = new FormData();
form.append('file', new Blob([bytes], { type: 'application/pdf' }), 'sample-pdf-100kb.pdf');
form.append('title', 'Quarterly report');
const response = await fetch('https://api.example.test/v1/documents', {
method: 'POST',
body: form,
});
console.log(response.status, await response.json());As with curl, do not set the Content-Type header by hand.
What to assert
A status code of 201 proves very little. A thorough upload test also checks:
- The response body. Does it report the size and type that you sent?
- The stored bytes. Download the file through the API and compare its SHA-256 checksum with the original's. This is the only proof that the content survived.
- Idempotency. If the API supports idempotency keys, send the same request twice and confirm that only one resource exists.
- Authorisation. Repeat the request without credentials, and with another user's credentials, and expect 401 and 403 or 404.
Computing a checksum for comparison:
sha256sum sample-pdf-100kb.pdf
curl -sS https://api.example.test/v1/documents/123/content | sha256sumError cases
Each of these should produce a 4xx response with an error body in the API's normal format. A 500 is a bug.
| Case | How to send it | Expected |
|---|---|---|
| Body too large | Use the 10 MB sample | 413 |
| Wrong type | Send a PNG to a PDF-only endpoint | 415 or 400 |
| Empty file | Send the zero-byte file | 400 or 422 |
| Missing file part | Omit the -F 'file=...' argument | 400 or 422 |
| Malformed JSON | Truncate a JSON sample with head -c | 400 |
| Mismatched type | Send a PDF declared as image/png | 415 or 400 |
For the malformed case:
head -c 5000 sample-json-100kb.json > truncated.json
curl -sS -o /dev/null -w '%{http_code}\n' \
-H 'Content-Type: application/json' \
--data-binary @truncated.json https://api.example.test/v1/importTesting with large JSON
For endpoints that accept JSON documents rather than files, size testing follows the same logic. The 100 KB JSON sample is exactly 102,400 bytes, the default limit of the Express JSON parser. The 1 MB sample exceeds most defaults and should be refused unless the limit has been raised on purpose.
To check how a parser copes with structure rather than size, use the nested configuration sample. It contains every JSON value type, escaped characters, an exponent, an empty object and an empty array.
Sample files as mock responses
The same files work in the other direction. While a back end is being built, a front end or a client library can be developed against static responses. The users JSON sample is an array of 50 objects in the shape of a typical list endpoint.
Any static file server will do. With Python installed:
python3 -m http.server 8000Then request http://localhost:8000/sample-users.json from your client code.
In a collection runner
Tools such as Postman, Bruno and Insomnia attach files to multipart requests through their interface and can run the same request across a list of files. Keep the fixtures in the same repository as the collection, and refer to them by relative path, so that the collection runs unchanged on a colleague's machine and in CI.