Migration Guide
Migrate an existing app to X
Move a Next.js or TanStack app to x piece by piece. x shares the file-based, React-islands model you already know, so most of your components, loaders, and API routes port over with minimal churn.
Conceptual mapping
x is built around the same ideas you already use, under slightly different names:
| Next.js / Remix | x | Notes |
|---|---|---|
| app/ or pages/ dir | src/pages | File-based routes; _layout.tsx layers nest by directory. |
| getServerSideProps / loader() | loader() | Same contract: async, receives params/request, returns data. |
| export const revalidate / ISR | export const revalidate | Identical semantics: static page + N-second cache. |
| Server Actions | Server functions | Async exports under src/actions become POST endpoints. |
| route.ts / api/ handlers | src/api/*.ts | Named GET/POST/PUT/PATCH/DELETE exports. |
| middleware.ts | Middleware | _middleware.ts or export const middleware; onion pattern. |
| Client components with hydration boundaries | Islands | export const islands in a page; <Island> hydrates them. |
1. Scaffold the shell
Create a fresh X project and copy your package files, styles, and public assets into it:
bun create thexjs-app@latest my-appcd my-appcp -r old-project/public .cp old-project/x.config.ts . 2>/dev/null || true
2. Port your data layer
Move schemas into versioned SQL migration files and run them at boot. x ships migration runners for both SQLite and Postgres that track applied files in a _x_migrations table:
CREATE TABLE users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT);import { connectSQLite, runSQLiteMigrations } from "@thexjs/core/data";import { join } from "node:path";const db = connectSQLite(process.env.SQLITE_PATH ?? "./data/app.db");runSQLiteMigrations(db, join(import.meta.dir, "..", "data", "migrations"));export default db;3. Port pages + loaders
A server-rendered page with a loader is almost identical to the Next.js version. Change the export name and drop the framework-specific glue:
import type { LoaderArgs, RouteProps } from "@thexjs/core";import db from "../lib/db";export async function loader({ params }: LoaderArgs) { const post = db.query("SELECT * FROM posts WHERE slug = ?").get(params.slug); if (!post) return { status: 404 }; return post;}export default function Post({ loaderData }: RouteProps) { const post = loaderData as { title: string; body: string }; return ( <article> <h1>{post.title}</h1> <p>{post.body}</p> </article> );}Static pages set export const mode = "static" and export const revalidate for time-based regeneration, just like ISR:
export const mode = "static";export const revalidate = 3600;4. Server functions
Fold per-route server actions into a shared module under src/actions. Call them from loaders, API routes, forms, or islands -- the client bundle only ships a fetch() wrapper, never the server implementation:
export async function subscribe(email: string) { const user = await db.query("SELECT id FROM users WHERE email = ?").get(email); if (!user) throw new Error("No account found for that email"); await db.run("INSERT INTO subscribers (email) VALUES (?)", [email]); return { ok: true };}import { subscribe } from "../actions/newsletter";export const islands = { Form };function Form() { return ( <form onSubmit={(e) => { e.preventDefault(); subscribe(new FormData(e.currentTarget).get("email")); }}> <input name="email" type="email" /> > <button>Subscribe</button> </form> );}5. API routes
Replace route.ts handlers with named export functions in src/api. Each receives the Request and returns a Response:
export function GET(request: Request) { return Response.json({ ok: true, ts: Date.now() });}export async function POST(request: Request) { const body = await request.json(); // ... return Response.json({ received: true }, { status: 201 });}6. Build, doctor, deploy
Run the diagnostics command to catch env-isolation violations, missing dirs, or dependency mismatches before you build, then ship like you did before:
x doctor # config, dirs, env isolation, depsx build # -> .x/client + .x/server (Bun server)x build --adapter vercel # -> .vercel/output Build Output API v3
What doesn't change
- React stays React -- x renders with react-dom server-side and hydrates islands client-side.
- Plain CSS, Tailwind (compiled by the dev server), and content collections slot in as-is.
- Your environment split: variables prefixed THEXJS_PUBLIC_ reach the client; everything else stays server-only.
- Deploy targets: a self-hosted Bun server, or Vercel via the bundled adapter.
Next step: read the Getting Started guide for the full walkthrough, or jump into Server Functions. If something from your framework doesn't map cleanly, open an issue -- the migration story is being actively tuned.