Uploads
upload() and uploadMany() send multipart over XMLHttpRequest, so you get progress events and a working cancel.
How do you show a progress bar? Not through fetch, which cannot report
upload progress. So these two methods take a different path from every other
method on the client.
Send one file
const asset = await client.collections.media.upload(file, {
onProgress: (percent) => setProgress(percent), // 0 to 100
signal: controller.signal, // abort to cancel
path: "uploads/2026", // optional, sets other fields on the row
});
console.log(asset.url);file is a browser File. It is sent as multipart form-data under the key
file, with path alongside it if you passed one. The server spreads path
onto the row it creates, which is how a blob lands inside a folder.
onProgress only fires while the browser can measure the body. Abort the
signal and the request stops, and the promise rejects.
Send several
const assets = await client.collections.media.uploadMany(files, {
onProgress: (overall, fileIndex) => setProgress(overall),
signal: controller.signal,
});uploadMany sends the files one at a time, not in parallel. onProgress
reports progress across the whole batch, with the index of the file being sent
now. It checks the signal between files, so a cancel stops the next one
starting. An empty array resolves to [] without touching the network.
What comes back
The created row, the same shape a create() would give you. On an upload
collection that includes url, resolved for you on read. These two methods are
the untyped corner of the client: they resolve to any, so nothing checks what
you read off the result.
The upload path skips SuperJSON
Multipart carries no SuperJSON header, so the server replies in plain JSON and
the client parses it with JSON.parse. A Date column on the returned row
arrives as a string. Read the row back with findOne if you need the typed
value.
What is thrown
Uploads throw UploadError, not QuestpieClientError. It carries status and
response when the server answered.
| Message | When |
|---|---|
Upload failed | Non-2xx. Replaced by the server's message |
Invalid response from server | A 2xx body that is not JSON |
Network error during upload | The request never completed |
Upload cancelled | You aborted the signal |
import { UploadError } from "questpie/client";
try {
await client.collections.media.upload(file);
} catch (err) {
if (err instanceof UploadError) {
console.error(err.status, err.message);
}
}Two things that surprise people
Every collection types upload and uploadMany, whether or not it stores
files. The check is on the server. Call them on a plain collection and you get
a 400 saying the collection does not support uploads. Add .upload() to the
collection to fix it. See Uploads.
Cookies still travel, because the request sets withCredentials. If you
configured getAuthHeaders, it runs here too and its headers go on the
request.
Next
Errors covers QuestpieClientError, which every
other method throws.