Astro is the only framework where the FastPix uploader is a native component rather than a React one. No React, no react-dom, no root to mount and unmount. It also means one thing works differently, and it's the thing everyone tries first. You can't pass the endpoint resolver as a prop. Astro serialises props when it renders on the server, and a function has no serialised form to send.
TL;DR
Astro is the only framework where the FastPix uploader is native rather than a React component. No React, no root to mount and unmount, no stylesheet to import. The one consequence is that endpoint can't be a prop: props are serialised during server rendering and functions aren't serialisable. Call getUploader("#up") in a browser <script> and assign endpoint as a property on the element it returns. Progress arrives as ordinary DOM events, so addEventListener is the whole API and sub-components need no provider.
Why Astro props cannot be functions
Astro renders components on the server and sends HTML. Props travel with that HTML, which means they have to survive being written to a string and read back.
Strings, numbers, booleans, arrays and plain objects survive that. A function doesn't, because it closes over scope that only exists in the process that created it.
So endpoint, which has to be a function so a URL is minted per upload rather than per page view, is assigned afterwards in the browser instead:
---
import { FastPixUploader } from "@fastpix/fp-astro-uploader";
---
<FastPixUploader id="up" accept="video/*" />
<script>
import { getUploader } from "@fastpix/fp-astro-uploader/client";
const uploader = await getUploader("#up");
uploader.endpoint = async () => {
const response = await fetch("/api/upload-url", { method: "POST" });
const { url } = await response.json();
return url;
};
</script>The <script> tag is the browser half. Astro runs it client-side, so the function exists where it needs to. The server route it calls is covered in why video in Astro needs an adapter.
You can prove the route against a real upload first. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.
| Prop type | Survives server rendering |
|---|---|
| id, accept, size | Yes |
| autoStart, appearance | Yes |
| endpoint, event callbacks | No, assign them in a script |
What you need before you start
- An Astro project version 7, with an adapter installed and
prerender = falseon the upload route. @fastpix/fp-astro-uploaderexcluded from Vite's dependency optimisation, which the next section explains.- A FastPix account with an Access Token ID and Secret Key from activate your account.
getUploader waits for the element to upgrade
A custom element in the DOM isn't necessarily upgraded yet. Before its definition loads it's an inert tag with the right name and none of the behaviour.
getUploader is the handshake for that. It waits for the element to be defined and upgraded, then hands you the element itself, on which endpoint is a property rather than an attribute.
Attributes are strings, which is why endpoint can't be one. Properties can hold any value, including a function, which is why the assignment happens on the object getUploader returns.
If that call throws, everything after it silently doesn't run, and the symptom is a confusing one:
Invalid config: - endpoint must be a non-empty string or a function; received undefinedThat error usually means the resolver was never assigned, because the getUploader line above it threw first. Read the browser console from the top rather than debugging the message you were given.
The getUploader error, and what it is really telling you
The error text is exact, and the fix is to read it literally rather than reach past it:
[fastpix] getUploader: the matched element is not an <fastpix-uploader>.That's exactly what it says: the element your selector matched isn't a <fastpix-uploader>. The only throw site in 0.1.0 is a tag-name check. Usually the id in getUploader("#up") doesn't match the id on the component, or the selector matched a wrapper you put around it.
Separately from that, Vite pre-bundles dependencies in development, and this package's client runtime has to be a single shared instance. The documented configuration excludes it:
// astro.config.mjs
export default defineConfig({
adapter: node({ mode: "standalone" }),
vite: {
optimizeDeps: {
exclude: ["@fastpix/fp-astro-uploader"],
},
},
});Vite hashes optimizeDeps into its cache key, so editing it invalidates the pre-bundle and the dev server restarts itself.
Astro uploader events are plain DOM events
Every other framework's uploader reports its lifecycle through callback props. The Astro one emits DOM events on the element, which means addEventListener and nothing else:
uploader.addEventListener("fastpix-progress", (e) => {
document.querySelector("#bar").value = e.detail.progress;
});
uploader.addEventListener("fastpix-success", () => {
// bytes delivered; encoding continues
});
uploader.addEventListener("fastpix-error", (e) => {
console.error(e.detail);
});fastpix-upload-start, fastpix-progress, fastpix-success and fastpix-error are the ones you'll use most. Because they're ordinary events they bubble, so a container can listen once for several uploaders rather than each one wiring its own handlers.
This is genuinely simpler than a callback-prop API. There's no framework state to synchronise, no re-render to think about, and the browser already knows how to do all of it.
Custom Astro uploader layouts need no provider
The layout side has no equivalent restriction, because the sub-components are custom elements rather than context consumers:
<FastPixUploader id="up" accept="video/*" autoStart={false} size="lg" appearance={{ accentColor: "#14CC80" }}>
<FastPixDropZone overlay>
<p>Drag a video here, or click to browse</p>
</FastPixDropZone>
<FastPixStatus />
<FastPixTrack showLabel />
<FastPixStartButton />
<FastPixPauseButton />
<FastPixResumeButton />
<FastPixAbortButton />
</FastPixUploader>Each child reads upload state from the host element, so placement and order are yours and nothing has to stay inside a provider. autoStart={false} holds the file until someone presses start, which is why a start button appears in this layout and not in the default one.
Compare that with SvelteKit, where the React uploader's sub-components read state through React context and therefore have to be composed with createElement inside a single render call. The Astro version has no such constraint, and that's the practical payoff of a native component.
Styles ship with the component, so there's no stylesheet import. That's one more difference from the React uploader, which needs styles.css imported or renders as an unstyled skeleton, as the SvelteKit wrapper route shows.
Start uploading with the Astro uploader
Render <FastPixUploader id="up" />, call getUploader("#up") in a <script>, assign endpoint as a property, and add the optimizeDeps exclusion before you start debugging anything. Progress comes back as fastpix-progress DOM events with no framework state involved. The free plan covers 10 videos and 100K streaming minutes a month, no credit card, which is enough to run a real upload through the whole event surface.
Frequently Asked Questions (FAQs)
Why cannot I pass a function as a prop in Astro?
Because Astro renders components on the server and props travel with the HTML, so they have to survive serialization. A function closes over scope that only exists in the rendering process and has no serialized form. Assign it as a property in a browser <script> instead, which is what getUploader exists for.
What does "Invalid config: endpoint must be a non-empty string or a function" mean?
It usually means the resolver was never assigned, because the getUploader call above it threw and everything after it stopped running. Read the browser console from the top and fix the first error rather than this one.
Why does getUploader say the matched element is not a fastpix-uploader?
Because the element your selector matched is not a <fastpix-uploader>. The only throw site in 0.1.0 is a tag-name check, so start with the id: the one in getUploader("#up") has to be on the component itself, not on a wrapper around it.
How do I track upload progress in Astro?
Listen for DOM events on the element getUploader returns. fastpix-progress carries the completion figure as detail.progress, and fastpix-upload-start, fastpix-success, and fastpix-error cover the rest of the lifecycle. Because they are ordinary events, they bubble, so one container listener can serve several uploaders.
Does the Astro uploader need React?
No. It is a native Astro component with its own custom elements, so there is no React, no react-dom, and no root to mount or unmount. Styles ship with the component, so there is no stylesheet to import either. Astro is the only framework where the uploader works this way.





