· 8 min read
Mixing It Up: How to Seamlessly Integrate React Remix with Existing Projects
A practical, step-by-step guide to progressively integrate React Remix into an existing codebase - keep your current functionality while gaining Remix's server rendering, routing, and modern developer ergonomics.
Why integrate Remix into an existing React project?
Remix brings server-side rendering (SSR), nested routes, first-class data loading (loaders), progressive enhancement, and a focused developer experience. For many teams with a running codebase, a full rewrite is both risky and expensive. The pragmatic answer is incremental adoption: bring Remix in where it yields the most value while keeping the rest of your app stable.
This guide is a hands-on, strategy-first walkthrough for integrating Remix into legacy or existing projects. You’ll learn practical patterns, example code, migration strategies, and a checklist you can follow route-by-route.
Key ideas we’ll cover
- Integration strategies (coexist, proxy, mount, micro-frontends)
- Server setup patterns (Node/Express, reverse proxy, adapters)
- Routing and navigation (mixing client and server routes, preserving URLs)
- Data and auth (sharing services, sessions, API vs. loaders)
- Assets, CSS and bundlers
- Testing, CI, and deployment considerations
- A migration plan and checklist
Useful references
- Remix documentation: https://remix.run/docs
- React docs: https://reactjs.org/
- Vite: https://vitejs.dev/
- Webpack: https://webpack.js.org/
- MDN History API: https://developer.mozilla.org/en-US/docs/Web/API/History_API
- Choose an integration strategy
There are several ways to add Remix to an existing app. Pick one based on risk tolerance, team bandwidth, and infrastructure.
- Side-by-side (subpath mount): Run your existing app and mount Remix at a subpath (e.g., /app or /remix). Low risk, fast to test.
- Proxy/edge routing: Use your existing server / reverse proxy to forward certain routes to a Remix service. Good for separation of concerns and gradual cutover.
- Middleware integration (Node apps): If your server is Node (Express, Fastify), mount Remix as middleware so both frameworks run in the same process.
- Micro-frontend: Use separate front-end bundles and mount them in the page (iframe or client-side integration). Higher complexity.
- Full replacement: Rewrite one piece at a time and swap routes directly - highest risk but cleanest single-process migration.
Most teams benefit from the subpath mount or proxy approach during early adoption.
- Mount Remix in a Node/Express app (example)
If your stack is Node/Express, you can run Remix side-by-side in the same process. This keeps your existing middleware, sessions, and logging while letting Remix handle specific routes.
Example (Express):
// server.js
const express = require('express');
const path = require('path');
const { createRequestHandler } = require('@remix-run/express');
const app = express();
// Serve your existing app's static assets as before
app.use('/static', express.static(path.join(__dirname, 'public')));
// Mount Remix at /remix (in prod, require the built remix server build)
if (process.env.NODE_ENV === 'production') {
const remixBuild = require('./build'); // output from `remix build`
app.all('/remix/*', (req, res, next) => {
// Optional: share session or user info via getLoadContext
createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV })(
req,
res,
next
);
});
} else {
// In dev, you might proxy dev server or use remix dev commands
// ...dev-time setup
}
// Your legacy routes still work
app.get('/legacy/*', (req, res) => {
// existing handler
res.send('Legacy content');
});
app.listen(3000);
Notes:
- createRequestHandler is the typical entry for Remix on Node. See the Remix docs for latest API details.
- Use a path prefix (/remix) so routing conflicts are minimized.
- Reverse-proxy approach (NGINX / load balancer)
If your existing app is not Node or you want clean service boundaries, use a reverse proxy. Example NGINX snippet:
server {
listen 80;
server_name example.com;
location /remix/ {
proxy_pass http://remix-service/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
proxy_pass http://legacy-service/;
}
}
This keeps deployments separate and lets you scale the Remix service independently.
- Handling routing and URLs
Remix uses server-side routes (file-based). When mixing with a client-side app:
- Avoid route collisions: Pick a clear prefix for Remix (e.g., /app or /remix) when starting out.
- Preserve query strings and route params when linking between legacy and Remix pages.
- For in-app navigation between legacy and Remix routes, use full-page links (anchor tags) rather than client-only routing to ensure the server handles the request.
- If you must do client-side transitions into Remix routes, ensure the client-side bootstrapping matches Remix expectations (hydrate the correct root element).
- Data access: share services, not adapters
You don’t need to move data-access logic into Remix immediately. Practical patterns:
- Shared service layer: Factor business logic (DB access, validation, domain services) into packages/modules both the legacy app and Remix can import.
- Keep API endpoints: Let Remix loaders call your existing internal APIs (REST or GraphQL) while you migrate.
- Server-side reuse: If Remix and your legacy server run in the same process, have Remix loaders call the same service functions or repositories used in legacy controllers.
Example loader calling a shared service:
// app/routes/dashboard.jsx
import { json } from '@remix-run/node';
import { getUserDashboard } from '~/services/dashboard.server';
export const loader = async ({ request }) => {
const user = await getUserFromRequest(request); // shared auth helper
const data = await getUserDashboard(user.id);
return json(data);
};
- Authentication and sessions
Auth is one of the trickiest parts of incremental adoption. Options:
- Share cookies/sessions: Keep the existing session cookie format and read it inside Remix via getLoadContext or a common session library. This requires both servers to be able to decrypt/verify session cookies.
- Use a centralized auth service (OAuth, Auth0, or an internal auth API). Both apps use the same token/cookie scheme.
- Proxy authentication via reverse proxy: Let an edge layer inject headers expressing user identity.
If you migrate to Remix gradually, choose the approach that requires the least change to your current session format, then refactor sessions later.
- Forms and progressive enhancement
Remix encourages standard HTML forms and progressive enhancement. When migrating individual forms:
- Convert server POST handlers to Remix actions, reusing the same validation and store logic.
- Keep client-side validation in legacy code while moving server-side validation into shared utilities.
- Test form flows end-to-end to ensure redirects and flash messages still work.
- Assets, bundlers and CSS
- Static assets: Point Remix’s public directory to the same static files or configure your existing static CDN to serve built Remix assets.
- Bundlers: If your legacy app uses Webpack and Remix uses its own bundler, keep both build pipelines. During migration, ensure unique file names or directories to avoid collisions (e.g., /static/legacy/_ and /build/remix/_).
- CSS: Remix supports many CSS strategies (global CSS, CSS modules, Tailwind). You can continue to serve global CSS from the legacy app and import component-level CSS in Remix. When co-located, avoid duplicate global resets.
- Testing and QA
- Unit tests: Share testable services. Keep behavior consistent by testing the same services used by both apps.
- Integration/E2E: Use Playwright or Cypress to test real user flows that cross legacy and Remix routes.
- Feature flags: Gate Remix features behind flags so you can safely enable/disable during rollout.
- CI/CD and deployments
- Single process deployment: If mounting Remix inside your existing server, add remix build steps to your CI and ensure the built files are included in deploy artifacts.
- Separate service deployment: Build and deploy Remix alongside the legacy app. Update the reverse proxy when ready to route to the new service.
- Rollbacks: Because Remix can be launched on a subpath, rolling back to legacy is often as simple as re-pointing the proxy.
- Adapters: When deploying to serverless or edge platforms, use the official Remix adapters/hosting docs: https://remix.run/docs (see hosting and deployment guides).
- Performance, caching and headers
- Edge caching: Cache Remix responses where appropriate (but respect per-route cache control when content is user-specific).
- CDN static assets: Keep static assets in your CDN or public bucket. Remix’s build can output fingerprinted files for long-term caching.
- Shared caching layer: If you had existing server-side caches (Redis), reuse them for loaders.
- Security and headers
- Cross-site cookies: When running on a different subdomain/service, ensure cookies have proper SameSite, domain, and secure flags.
- CSP and headers: Ensure your Content Security Policy, HSTS, and other security headers are consistent across both apps.
- Migration patterns: route-by-route example
A common safe path to migrate functionality route-by-route:
Choose a small, isolated route that benefits from Remix (e.g., marketing page or user dashboard).
Create a Remix route under /remix/your-route and mount Remix at /remix.
Implement the loader/action using shared services.
Wire up navigation between legacy pages and Remix page using normal anchors.
Smoke test, add feature flags, monitor logs and metrics.
If successful, migrate the next route. Eventually you can flip the proxy to remove the /remix prefix and move routes to the root.
Real-world pitfalls and how to avoid them
- Session format mismatch: Coordinate session cookie encoding between apps before migrating protected routes.
- Route collisions: Always test routing on staging - history API differences can cause surprising client-side 404s.
- Asset name conflicts: Separate static output directories or use different URL prefixes.
- Unexpected SSR assumptions: If legacy pages rely on global window properties on first paint, be careful when Remix renders the same page on the server - ensure code guards for window usage.
- Migration checklist (quick)
- Pick integration strategy (subpath, proxy, or middleware)
- Set up Remix project and build pipeline
- Configure server (Express mount or reverse proxy)
- Share authentication/session strategy
- Share or extract data services and utilities
- Migrate one route and test end-to-end
- Add monitoring and feature flags
- Gradually expand migration surface
- Clean up legacy code as routes are fully migrated
Conclusion
Integrating Remix into an existing project is a very achievable goal with the right plan. Start small, protect user-facing behavior, and share business logic rather than duplicating it. Use subpath mounts or a proxy for a low-risk start, and only refactor global pieces (auth, sessions, assets) when you have confidence and tests in place.
By following the patterns above - middleware mounting, reverse-proxy routing, shared service layers, and a route-by-route migration plan - you can gain Remix’s advantages without destabilizing the system your users rely on.
Further reading
- Remix documentation: https://remix.run/docs
- React docs: https://reactjs.org/
- MDN on the History API (for client-side navigation concerns): https://developer.mozilla.org/en-US/docs/Web/API/History_API