September 16, 2026

Where your API lives in a Vite React app

D. Neha Reddy
D. Neha Reddy
Software Engineer

Vite runs a development server and ships a bundler. It doesn't run a server in production, and vite build emits static files with nothing behind them. Put those two facts together and you get the most common Vite deployment failure. Every /api call works on localhost, then returns 404 the moment it's deployed. The proxy that made it work was a development convenience, and nothing replaced it.

TL;DR

Vite is a build tool, not a framework. vite build emits static files with no runtime behind them, so server.proxy disappears at build time and every /api call 404s in production. The VITE_ prefix is the more dangerous half. It inlines a value into the bundle at build time and ships it to every visitor, so prefixing a Secret Key publishes it. Three things need a server: anything holding a credential, anything receiving a webhook, and anything the client must not be able to lie about. Everything else stays on the CDN.

Vite is a build tool, not a framework

Next.js, Remix and SvelteKit all ship a server as part of the framework, so "where does the API live" has a default answer. Vite ships a bundler, so it doesn't.

That's a design choice rather than a gap. A Vite app is a folder of HTML, JavaScript and CSS that any static host will serve. That's why it deploys to a CDN with no runtime and starts instantly. The cost is that anything needing a secret, a database or a webhook has nowhere to run.

ConcernNext.js, Remix, SvelteKitVite
Server in productionPart of the frameworkNone
Secret storageServer-side environmentNowhere in the app
Receives webhooksYes, a routeNo
/api routesFramework routingWhatever you deploy separately
Deploy targetA runtimeA static host

What you need before you start

  • A React app created with Vite, version 18 or later.
  • A place to run a small server. Express on a container, a serverless function, or an existing backend all work.
  • A FastPix account with an Access Token ID and Secret Key from activate your account, if you're following the video example.

The VITE_ prefix is a publishing decision

Vite only exposes environment variables prefixed with VITE_ to your client code, and that rule is usually described as a convenience. It's actually a warning label.

A VITE_-prefixed variable isn't read at runtime. Vite inlines it into the bundle at build time, so wherever your code reads import.meta.env.VITE_THING, the literal string is what ships. Current Vite drops variables nothing references, so an unused one usually won't appear in the output, but that's dead-code elimination rather than a security boundary: the moment any code path reads it, the value is in a JavaScript file served to every visitor. Prefixing a Secret Key with VITE_ because the app couldn't see it otherwise is therefore not a workaround. It's publication.

bash
# .env
FASTPIX_USERNAME=your-access-token-id   # server only, never reaches the bundle
FASTPIX_PASSWORD=your-secret-key        # server only, never reaches the bundle

VITE_WORKSPACE_KEY=wk_...               # fine: a public analytics key, meant to ship

The distinction isn't "secret or not secret". It's whether the value is safe in the hands of anyone who opens devtools. Analytics workspace keys and public playback IDs are designed for that. Signing keys and API secrets aren't.

Step 1: Understand what the dev proxy actually does

Vite's server.proxy forwards matching requests from the dev server to another origin. The browser sees same-origin calls, and CORS never comes up:

typescript
// vite.config.ts
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { "/api": "http://localhost:8787" },
  },
});

You can run this against a real upload before committing to it. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.

This is genuinely useful and worth using. What it isn't is part of your application. The proxy lives in vite.config.ts, which is build tooling, and vite build doesn't emit it, so it exists only while vite dev is running.

Two processes therefore have to be running in development: Vite, and whatever localhost:8787 is. A 404 on /api in development almost always means the second one isn't.

Step 2: Choose where the API runs in production

Three arrangements work, and they differ in how much CORS you have to think about rather than in capability.

Same origin behind one reverse proxy. Serve the built dist/ folder and the API from the same hostname. Route /api to the API and everything else to the static files. The browser sees one origin, so no CORS headers are needed and the production setup matches development most closely.

Separate origin with CORS. Deploy the static build to a CDN and the API somewhere else, then set Access-Control-Allow-Origin on the API to the site's origin. Simple to deploy, and it introduces preflight requests plus a credentials decision you now have to get right.

Serverless functions alongside the static build. Most static hosts run functions from a conventional directory next to the assets, on the same origin. This is the lowest-effort option when your server-side needs are two or three small routes.

The last one is usually right for a Vite app. The whole reason to pick Vite over a full-stack framework is that the server-side surface is small.

Step 3: Keep the server surface small on purpose

The point isn't to rebuild a backend. It's to identify the specific operations that can't happen in a browser, and move only those.

Anything holding a credential, because a credential in the bundle is a published credential. Anything an external service calls, since webhooks need a URL that isn't a browser tab. And anything the client must not be able to lie about, such as deciding whether this particular user is allowed to watch this particular video.

Everything else stays in the client, where Vite is fast and the deployment is a CDN.

Where the server boundary falls for video

Video makes the boundary unusually concrete, because uploading and playing both start with an operation the browser can't perform.

Creating an upload URL needs the Secret Key, so it's a server route. The browser then uploads directly to that URL. The file bytes never pass through your infrastructure at all:

javascript
// server.js
app.post("/api/upload-url", async (_req, res) => {
  const upload = await fastpix.inputVideo.upload(
    { corsOrigin: "*", pushMediaSettings: { accessPolicy: "public" } },
    { headers: { "X-Client-Type": "web-browser" } },
  );

  res.json({ url: upload.data.url, mediaId: upload.data.uploadId });
});

The X-Client-Type: web-browser header is about who pushes the bytes, not who requests the URL. Your server asks for it and the browser uploads to it, so the header is required. Without it, uploads fail with error code 0 after five attempts while the media still processes, which is documented in the React (Vite) guide.

Knowing when the video is playable needs the second server concern. Encoding finishes asynchronously and FastPix announces it with a video.media.ready webhook, which needs a public URL. A browser tab isn't one, so during development you either tunnel or poll the media with fastpix.manageVideos.get({ mediaId }) and wait for status === "Ready". Webhook setup and signature verification are in set up webhooks.

Playback is the part that needs no server at all, as long as the access policy is public. The playback ID is safe in the client. <fastpix-player> is a custom element and renders in React as a plain tag:

text
import "@fastpix/fp-player";

<fastpix-player playback-id={playbackId} stream-type="on-demand" />;

Private playback moves back across the line. A signed token has to be minted with a private key. That belongs on the same small server, and we cover the pattern in signed video URLs in a React app.

Run your first signed upload from a Vite app

Add one Express route that calls fastpix.inputVideo.upload() with the X-Client-Type: web-browser header, proxy /api to it in vite.config.ts, and point a <FastPixUploader> at the route. That's the entire server-side surface for uploads, and the file bytes still go straight from the browser to FastPix. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Test the whole boundary before deciding where to deploy the API.

Frequently Asked Questions (FAQs)

Does Vite have a backend?

No. Vite is a build tool: vite dev runs a development server, and vite build emits static HTML, JavaScript and CSS with no runtime behind them. Anything that needs a secret, a database or a webhook endpoint has to be deployed separately, such as a small server or serverless functions alongside the static build.

Why does the Vite proxy work in dev but not in production?

Because server.proxy is configuration for the Vite dev server. That dev server does not exist after vite build. The built output is static files, so nothing forwards /api anywhere. Fix it by serving the build and the API from the same origin behind a reverse proxy, or by enabling CORS on a separately deployed API.

Is it safe to put an API key in a VITE_ environment variable?

Only if the value is meant to be public. Vite inlines every VITE_-prefixed variable into the client bundle at build time, so the literal string is served to every visitor and visible in devtools. Public analytics keys and playback IDs are designed for that. Signing keys and API secrets are not, and prefixing one with VITE_ publishes it.

How do I call an API from a Vite React app without CORS errors?

In development, add server.proxy to vite.config.ts so the browser sees same-origin requests. In production, serve the static build and the API from the same hostname behind one reverse proxy, which removes CORS entirely. If they have to live on different origins, set Access-Control-Allow-Origin on the API and handle preflight requests.

Can a Vite app receive webhooks?

No. A webhook needs a publicly reachable URL that accepts POST requests, and a static build has no server to accept them. Deploy a small endpoint separately, and while developing, either expose it with a tunnel or poll the resource's status directly instead.

Share

Stay Ahead of Video
Streaming Trends

Start shipping video today.