September 17, 2026

Migrating a Remix video upload to React Router v7

Ashutosh Khainar
Ashutosh Khainar
Content Marketing

Remix v2 was upstreamed into React Router and is in maintenance mode, so create-remix now points new projects at create-react-router. For an existing video upload feature that news is less dramatic than it sounds. The loader that fetches a playback ID is the same code. So is the action that mints a signed upload URL, and the resource route that holds it. Three things around them change, and only one of them takes real thought.

TL;DR

Loaders, actions, resource routes and the .server.ts convention all survive the move unchanged, and the FastPix packages don't change at all. Three things do. Routes move from filename conventions into explicit entries in app/routes.ts. Imports go from @remix-run/* to react-router. And route types now come from react-router typegen rather than inference. The failure to watch for is a route left out of routes.ts, because it 404s silently and the webhook route is the usual casualty. Staying on Remix v2 is fine; maintenance mode means fixes rather than a deadline.

What survives a Remix to React Router v7 migration

The reason this migration is small is that Remix's core ideas became React Router's core ideas rather than being replaced.

A loader is still a loader. An action is still an action. A resource route is still a route module with no default export. The server-only .server.ts convention still applies, and the FastPix packages don't change at all.

ConceptRemix v2React Router v7
Loader and actionSameSame
Resource routeNo default exportNo default export
Server-only module.server.ts.server.ts
FastPix SDK, uploader, playerUnchangedUnchanged
Route declarationFile-name conventionsExplicit in routes.ts
Imports@remix-run/node, @remix-run/reactreact-router
Route typesInferredGenerated by typegen

What you need before you start

  • An existing Remix v2 application with a working video upload flow.
  • A FastPix account with an Access Token ID and Secret Key from activate your account.
  • A branch, because the routing change touches every route file at once.

Change one: Remix routes move into routes.ts

This is the only part that requires thought, because it replaces a convention with a declaration.

Remix v2 derived URLs from filenames. app/routes/api.upload-url.ts became /api/upload-url because of the dots in its name. React Router's Framework Mode declares them in app/routes.ts instead:

typescript
// app/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("upload", "routes/upload.tsx"),
  route("play/:mediaId", "routes/play.tsx"),
  route("api/upload-url", "routes/api.upload-url.ts"),
  route("api/fastpix/webhook", "routes/api.fastpix.webhook.ts"),
] satisfies RouteConfig;

The filenames can stay exactly as they are. Dots in a name are now just a readability convention, and the URL comes from the first argument to route(). The greenfield version of the same setup is in large video uploads in React Router v7.

You can exercise the whole path on the migration branch. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.

If you would rather not hand-write that list, you don't have to. @react-router/fs-routes restores Remix's filename routing, and flatRoutes() in routes.ts reads the same app/routes directory the old convention did. There's also @react-router/remix-routes-option-adapter for projects that used a custom route config, and a codemod at npx codemod remix/2/react-router/upgrade that does most of the mechanical work.

The migration failure to expect is a route that 404s because it was never added. Nothing warns you: the file exists, the code is correct, and the URL doesn't. Webhook routes are the usual casualty. Nobody visits them manually, so the failure only surfaces when the ready event arrives and nothing marks the video ready.

Change two: @remix-run imports become react-router

Mechanical, and a find-and-replace handles most of it. @remix-run/node and @remix-run/react become react-router.

The FastPix side of your upload code is untouched. It imports from @fastpix/fastpix-node, @fastpix/fp-react-uploader and @fastpix/fp-player in both worlds:

typescript
// app/fastpix.server.ts, identical before and after
import { Fastpix } from "@fastpix/fastpix-node";

export const fastpix = new Fastpix({
  security: {
    username: process.env.FASTPIX_USERNAME,
    password: process.env.FASTPIX_PASSWORD,
  },
});

The .server.ts suffix keeps doing its job. The VITE_ prefix is still the thing that would publish your Secret Key if you used it. Both frameworks build with Vite, so that hazard is unchanged.

Change three: React Router route types are generated

Remix inferred types from the route module. React Router generates them from routes.ts, which means a build step stands between editing a route and having correct types.

text
// app/routes/play.tsx
import type { Route } from "./+types/play";
import { fastpix } from "../fastpix.server";
import Player from "../components/Player";

export async function loader({ params }: Route.LoaderArgs) {
  const media = await fastpix.manageVideos.get({ mediaId: params.mediaId! });

  if (!("data" in media)) throw new Response("Media not found.", { status: 404 });

  return { playbackId: media.data!.playbackIds![0].id! };
}

export default function Play({ loaderData }: Route.ComponentProps) {
  return <Player playbackId={loaderData.playbackId} />;
}

Property 'mediaId' does not exist and a missing ./+types/... module mean the same thing. Run react-router typegen, or npm run typecheck, which runs it first. The dev server reloads routes.ts on save and regenerates the types with it, so no restart is needed.

What staying on Remix v2 costs you

Maintenance mode isn't a deadline, and there's a version of this decision where you do nothing for now.

On an existing Remix v2 codebase the FastPix integration still works, with a translation. Import from @remix-run/node and @remix-run/react and register routes with file conventions. The loader and action shapes carry over, but the generated ./+types/* modules and the loaderData prop shown in the React Router docs don't exist in v2: use useLoaderData() and type the return yourself.

What you don't get is new work. Maintenance mode means fixes rather than features, so anything added to the framework from here lands in React Router. That's an argument for migrating eventually rather than urgently.

Verify the video upload flow end to end after migrating

The webhook route is the one to check first. It's the route most likely to be forgotten in routes.ts, and its failure mode is silence. Uploads succeed, encoding completes, and nothing ever marks the video ready. Post to it manually, or upload a real file and watch for the record to update.

The .server.ts boundary is the other one, and it's stricter than it looks. If a route component reaches a server-only module, the build fails with Server-only module referenced by client rather than quietly bundling the key. After a migration that touched every route file, that error is the one to expect, and it's telling you about a real import path rather than a false alarm.

Move your upload routes across

Declare every route in routes.ts, the webhook one included. Rename the framework imports and run react-router typegen. Then upload a real file and confirm the record actually flips to ready. The FastPix code itself needs no changes. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Exercise the whole path on the migration branch before it merges.

Frequently Asked Questions (FAQs)

Is Remix the same as React Router v7?

Effectively yes. Remix v2 was upstreamed into React Router and is in maintenance mode, and create-remix now directs new projects to create-react-router. Loaders, actions and resource routes work the same way. What differs is that routes are registered in routes.ts rather than derived from filenames, and imports come from react-router instead of @remix-run/*.

Do I need to rewrite my video upload code to migrate to React Router v7?

No. The resource route that mints the signed upload URL, the loader that fetches the playback ID and the .server.ts module holding credentials are all unchanged. You register the routes in routes.ts, rename the framework imports and run typegen. The FastPix packages are identical in both.

Why does my route 404 after migrating from Remix?

Because config-based registration is the Framework Mode default, so the file still exists and the URL does not until you add it to app/routes.ts. The dev server picks up that edit without a restart. Webhook routes are the usual casualty. Nobody visits them manually, so the failure only appears when a video finishes encoding and nothing marks it ready. If you would rather keep filename routing, @react-router/fs-routes restores it.

Can I still use FastPix with Remix v2?

Yes, with one translation. Import from @remix-run/node and @remix-run/react and register routes with file conventions. The loader and action shapes carry over, but ./+types/* and the loaderData prop are React Router 7 features: on v2, use useLoaderData() instead. Maintenance mode means the framework gets fixes rather than features, which is an argument for migrating eventually rather than immediately.

What breaks quietly during this migration?

A route left out of routes.ts 404s with no warning, which is worst for the webhook route because uploads still appear to work. And a .server.ts module reached from a route component fails the build with Server-only module referenced by client, which is the boundary doing its job rather than a problem with your migration.

Share

Stay Ahead of Video
Streaming Trends

Start shipping video today.