Migration from Next.js
A step-by-step migration guide for moving a serious full-stack application from Next.js ecosystem to Nue and web standards. This covers hybrid MPA+SPA architectures with content sites, dynamic applications, and backend integration.
What to expect
After migration you'll see:
90% less project scaffolding - From hundreds of megabytes of NPM modules and dozens of configuration files (TypeScript, ESLint, Tailwind, PostCSS, Webpack) to minimal configuration and zero redundancy.
Cleaner and smaller codebase - Similar to simplified project setup, your monolithic components become leaner with architectural clarity. App concerns live in isolated layers that can be managed and scaled independently.
Lightweight pages and apps - Order of magnitude smaller footprint across all pages. Content-heavy marketing sites, documentation, blogs, and full single-page applications all become dramatically lighter. We're talking single-page apps that take less bandwidth than a single React button component.
Faster builds - Build times drop from seconds to milliseconds. HMR spans all pages, apps, assets, and server routes. Every update takes around 20ms.
Project setup
When moving to Nue, start with a fresh project structure. The monolithic Next.js architecture where everything lives in components doesn't translate directly to Nue's separated concerns. The focus is on cleaning up the entire architecture rather than porting it file-by-file.
The first step is cleaning up unnecessary NPM modules, project scaffolding, and configuration. This is quite significant in Next.js. An empty Next.js project created with npx create-next-app@latest
(v15.5) contains 336 packages, 18,666 files, and 427MB. A size of eight Windows 95 installations.
Add a component library like ShadCN and you're at 470MB (9x Windows 95). A typical Next.js project has at least the following configuration files to start with:
.
├── app/
│ └── page.tsx
├── components/
│ └── ui/
│ └── button.tsx
├── components.json
├── eslint.config.mjs
├── lib/
│ └── utils.ts
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
└── tsconfig.json
With Nue:
.
├── index.css
└── index.html
No configuration required. Nue installs globally with bun install -g nuekit
and works like a UNIX command (nue
, nue build
, nue --help
). Your project stays clean.
The shift is from configuration theater with third-party syntaxes to minimalism and web standards. You lose no functionality - only complexity. The index.html
can be dynamic or server-rendered, contain expressions and event handlers, or serve as your SPA entry point. It's a clearer foundation for both developers and AI models to understand and build upon.
Content
Migrating from monolithic components and MDX to a content-first architecture.
Next.js: 500MB of redundancy
Since Next.js provides no content authoring tools natively, you need these additional packages for a content-focused site with blogs, documentation, and marketing pages:
# MDX, MD extensions, content processing and view transitions
npm install /mdx /loader /react
npm install gray-matter contentlayer next-contentlayer
npm install remark-gfm rehype-autolink-headings rehype-slug
npm install shiki date-fns next-seo feed next-sitemap
npm install framer-motion
This causes your repository size to balloon from 470MB to a whopping 550MB, over 500 times of Nue without even reaching the complete feature set yet.
Next.js content architecture
After implementing a handful of blog entries, documentation, and marketing pages, your Next.js project structure looks like this:
.
├── app/
│ ├── blog/
│ │ ├── [slug]/
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── docs/
│ │ ├── [...slug]/
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── about/
│ │ └── page.tsx
│ ├── pricing/
│ │ └── page.tsx
│ └── layout.tsx
├── content/
│ ├── blog/
│ │ ├── first-post.mdx
│ │ ├── design-systems.mdx
│ │ └── web-standards.mdx
│ └── docs/
│ ├── getting-started.mdx
│ ├── api-reference.mdx
│ └── deployment.mdx
├── components/
│ ├── ui/
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── badge.tsx
│ ├── blog-post.tsx
│ ├── doc-layout.tsx
│ └── hero-section.tsx
├── lib/
│ ├── content.ts
│ └── utils.ts
├── components/
│ ├── page-transition.tsx
│ └── variants.ts
├── hooks/
│ └── use-router-events.ts
├── contentlayer.config.js
├── next.config.js
├── mdx-components.tsx
├── package.json
├── tsconfig.json
└── [8 other config files...]
Nue migration
Nue gives you the same functionality with this structure:
.
├── layout.html
├── components.html
├── docs/
│ ├── layout.html
│ ├── getting-started.md
│ ├── api-reference.md
│ └── deployment.md
├── blog/
│ ├── layout.html
│ ├── first-post.md
│ ├── design-systems.md
│ └── web-standards.md
├── about.md
├── pricing.md
└── index.md
Nue configuration
Enable all the features (collections, RSS feeds, sitemaps, view transitions) with this site.yaml
config:
site:
view_transitions: true
sitemap: true
content:
heading_ids: true
sections: true
collections:
blog:
match: [blog/*.md]
sort: date desc
rss: true
docs:
match: [docs/*.md]
sort: order asc
This literally drops thousands of lines of code, configuration and scaffoling to get a versatile content engine running under Next.js.
Nue benefits
Massive simplification - After content migration your file system goes from 30+ files across 8 directories with complex interdependencies to 11 clean files organized by purpose. No routing logic, no component glue code, no build configuration.
Scalable content - All pages, from rich front pages to simple blog entries, are editable with pure content. Page development becomes a content project, not a software engineering and TSX debugging project. Writers work independently without breaking builds.
All features in 1MB - Syntax highlighting, heading links, collections, RSS feeds, sitemaps, view transitions, responsive images, layout inheritance, and content processing.
Rich layouts - With slots and layout modules, create sophisticated page structures without component hierarchies. Section-specific layouts inherit and override automatically.
Backend
Migrating your backend infrastructure (server and databases) from third-party APIs to edge first approach and web standards.
Next.js: More packages
The backend landscape is fragmented with options ranging from the complex T3 stack to newer Server Actions. This guide assumes tight integration with the Vercel ecosystem using these dependencies, after which the project size reaches 1.4G:
npm install /kv /postgres
npm install drizzle-orm drizzle-kit
npm install next-auth@beta /drizzle-adapter
npm install /node
This requires at least the following TypeScript configuration files:
.
├── drizzle.config.ts # Database schema config
├── auth.config.ts # Auth.js configuration
├── middleware.ts # Route protection
├── .env.local # Database URLs and secrets
├── lib/
│ ├── auth.ts # Auth setup
│ ├── db.ts # Database connection
│ └── schema.ts # Drizzle schema
├── app/api/
│ ├── auth/[...nextauth]/
│ └── users/
│ └── route.ts # API endpoints
└── __tests__/
├── auth.test.ts
└── api.test.ts
Next.js: Non-standard APIs
Your server code becomes tightly coupled to framework abstractions:
// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { db } from "@/lib/db"
import { eq } from "drizzle-orm"
import { users } from "@/lib/schema"
These are proprietary APIs, not web standards. Your code only works within the Next.js ecosystem.
Nue migration
Nue skips NPM installs and configuration files. Jump straight to development using the global 1MB nue executable:
server/
├── index.js # Server routes
├── db/ # Database files
│ ├── app.db # SQLite database
│ ├── kv.json # KV store data
│ └── init/ # Schema and sample data
├── model/ # Business logic
│ ├── auth.js # Authentication
│ ├── index.js # Data operations
│ └── utils.js # Utilities
└── test/ # Test suites
├── mock.js # Mock environment
├── model.test.js # Business logic tests
└── server.test.js # Integration tests
Nue server setup
Configure the entire system centrally in `site.yaml
server:
dir: server # Server directory
db: db/app.db # SQL database location
kv: db/kv.json # KV database location
reload: true # Server route HMR
Edge first
Write code that works identically locally and globally:
// Authentication middleware
use('/admin/*', async (c, next) => {
// Same API locally and on CloudFlare Edge
const { KV, DB } = c.env
// CloudFlare headers work locally too
const country = c.req.header('cf-ipcountry')
const user = await KV.get(`session:${sessionId}`, { type: 'json' })
if (!user) return c.json({ error: 'Unauthorized' }, 401)
await next()
})
// API routes with standard Request/Response
get('/api/users', async (c) => {
const { DB } = c.env
const users = await DB.prepare('SELECT * FROM users').all()
return c.json(users)
})
Nue benefits
90% less boilerplate - No TypeScript configs, no authentication setup files, no database connection boilerplate, no middleware configuration.
Edge first architecture - Server routes, KV storage, and SQL databases work seamlessly locally and globally with identical APIs.
Integrated development - Frontend and backend on same port with nue dev
. Instant startup, built-in HMR for all server routes and database changes.
Web standards - Work with standard Request
/Response
objects, not framework abstractions. Code that runs anywhere.
Business logic
On this step, our goal is to build an isolated, testable business logic layer that works independently of any UI framework. We use plain JavaScript or TypeScript to create a portable model that is free from frontend concerns.
Next.js: More packages
Next.js has no notion of a decoupled business logic layer. Instead there are multiple options to integrate business logic into your components. Tools like Redux Toolkit, RTK Query and Formik. Or SWR, Valtio, and React Final Form. However the most popular stack currently is likely TanStack Query, Zustand and React Hook Form. This has become the de facto standard for modern React applications in 2024-2025. So let's add some more packages:
npm add /react-query zustand react-hook-form
npm add -D /react-query-devtools
npm add /resolvers zod
Next.js: Non-standard APIs
With Next.js you are using non-standard APIs and mixing business logic, state management, framework patterns, and rendering together:
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1, 'Name required'),
// validation rules...
})
export default function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
})
const updateMutation = useMutation({
mutationFn: (data) => fetch(`/api/users/${userId}`, {
method: 'PUT',
// API logic...
}),
onSuccess: () => {
queryClient.invalidateQueries(['user', userId])
// cache invalidation logic...
}
})
return (
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
<form onSubmit={handleSubmit((data) => updateMutation.mutate(data))}>
// rendering logic ...
</form>
</div>
)
}
This approach tangles business logic with UI concerns. Data fetching, validation, caching, and rendering all live in the same component. Testing becomes complex (or impossible) because you can't test business logic without mounting React components.
Nue migration
Nue separates business logic into pure, testable modules. Your application model lives independently of any UI framework:
/
├── app/
│ ├── index.js # Main app exports
│ ├── users.js # User operations
│ ├── payments.js # Payment processing
│ └── analytics.js # Analytics tracking
└── test/
├── users.test.js # Unit tests
├── payments.test.js
└── analytics.test.js
Configure the import map in site.yaml
:
import_map:
app: //app/index.js
Pure business logic
The application code is pure JavaScript with no frontend concerns:
// snippet from `nue create full`
export async function login(email, password) {
const ret = await post('/api/login', { email, password })
localStorage.$sid = ret.sessionId
}
export async function postContact(data) {
return await post('/api/contacts', data)
}
export async function getContacts(params) {
return await get('/admin/contacts', params)
}
Nue benefits
Architectural clarity - Business logic, data operations, and validation live separately from UI components. Each layer can be developed independently.
Testability - Unit test your application logic without mixing frontend concenrs. Pure functions are trivial to test.
Portability - Your business model works with any UI layer. Migrate from React to Vue to vanilla JavaScript (or TypeScript) without rewriting core application logic.
Future-proof architecture - Pure JavaScript stays relevant forever. No trendy frontend tools risk making your model outdated.
Advanced possibilities - Decoupling enables ambitious logic engines built in Rust or Go, like those from Figma or Notion.
UI development
On this step, we migrate all React components into clean, semantic HTML and detach all business logic and styling, leaving the code focused on structure only. This makes UI development similar to content development: rapid assembly of interfaces.
Next.js: Mixed concerns
React components mix business logic, styling, data fetching, validation, and rendering together in a single file. For example:
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useMutation } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
// Validation schema mixed with UI component
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters')
})
type LoginFormData = z.infer<typeof loginSchema>
export default function LoginForm() {
// State management hooks scattered throughout component
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const router = useRouter()
// Form validation library integration
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormData>({
resolver: zodResolver(loginSchema)
})
// Business logic embedded in component
const loginMutation = useMutation({
mutationFn: async (data: LoginFormData) => {
// API call logic mixed with component
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
return response.json()
}
})
// Event handlers with side effects
const onSubmit = async (data: LoginFormData) => {
// Authentication logic inside UI component
await loginMutation.mutateAsync(data)
router.push('/app/')
}
// Styling through utility classes and pre-built components
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<Card className="w-full">
<CardHeader>
// Semantic HTML buried under framework abstractions
<CardTitle className="text-2xl font-bold text-center">
Sign in to your account
</CardTitle>
</CardHeader>
// etc...
</Card>
</div>
</div>
)
}
This monolithic component mixes concerns together:
Business logic - API calls, authentication, token storage
Styling - Tailwind classes, conditional styling, layout positioning
Validation - Zod schemas, form validation, error handling
State management - Multiple useState hooks, useEffect lifecycle
UI structure - Form elements, labels, buttons buried in framework abstractions
There are almost as many ways to create a React form as there are developers, because the tools and patterns evolve quickly. So this example might not represent idiomatic
React, but it demonstrates the fundamental issue: concerns are inevitably mixed together.
Nue: Semantic HTML
Nue separates UI structure from all other concerns. Components focus purely on semantic HTML and user interactions:
<script>
import { login } from 'app'
</script>
<form :onsubmit="submit">
<!-- UI code goes here-->
<label>
<h3>Email</h3>
<input name="email" type="email" value="admin@example.com"
autofocus autocomplete="email" class="fullsize">
</label>
<label>
<h3>Password</h3>
<input name="password" type="password" value="demo123"
autocomplete="current-password" class="fullsize">
</label>
<!-- event handlers here -->
<script>
async submit(e) {
const { email, password } = e.target
try {
// handlers call methods in your business model
await login(email, password)
location.href = '/app/'
} catch (error) {
this.update({ error: 'Invalid credentials' })
}
}
</script>
</form>
Nue benefits
Standards first - Components use semantic HTML elements (<form>
, <table>
, <button>
) instead of framework abstractions.
Immediate productivity - New team members can contribute immediately. HTML knowledge transfers directly.
Application assembly - With concerns separated, building interfaces becomes assembly work. Import business functions, write semantic HTML, let the design system handle presentation.
Future-proof - HTML semantics outlast frameworks. Your <form>
elements will work in browsers 20 years from now. React components from 2020 already feel outdated.
Styling
Styling is the most important migration point for building maintainable and scalable products. This final migration step moves from hardcoded styling monoliths to a modern standards-based design system.
Next.js: hardcoded styling
The React ecosystem promotes mixing styling directly into components. While multiple approaches exist, the current trend seems to combine Tailwind, ShadCN/UI, clsx, and tailwind-merge. Something like this:
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs))
}
export function ProductCard({ product, isFeature, variant, className }) {
return (
<div className={cn(
"bg-white border rounded-lg p-4 shadow-sm",
isFeature && "border-blue-500 shadow-blue-100",
variant === "compact" && "p-2",
variant === "featured" && "border-2 shadow-lg",
className
)}>
<h3 className={cn(
"font-semibold text-gray-900",
isFeature && "text-blue-700",
variant === "compact" && "text-sm"
)}>
{product.name}
</h3>
<button className={cn(
"bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700",
isFeature && "bg-gradient-to-r from-blue-600 to-purple-600",
variant === "compact" && "px-2 py-1 text-sm"
)}>
Add to Cart
</button>
</div>
)
}
This approach stems from fears of global namespace pollution, desire for co-location, and challenges with naming conventions. Every component becomes a styling puzzle requiring utility memorization and merge logic.
Nue: design system
Nue embraces minimal and semantic design systems that are centrailly maintained. You can return to clean, isolated CSS code structured as a proper system and avoid all problems that drove developers to CSS-in-JS:
/design/
├── base.css # Typography, colors, spacing
├── button.css # All button variants
├── content.css # Blog posts, documentation
├── dialog.css # Modals, popovers
├── document.css # Page structure
├── form.css # All form elements
├── layout.css # Grid, stack, columns
├── syntax.css # Code highlighting
├── table.css # Data tables
└── apps.css # SPA-specific components
The same product card becomes pure structure:
<article class="card featured">
<h3>{ product.name }</h3>
<button>Add to Cart</button>
</article>
CSS handles all presentation decisions in one place. Variants, states, and responsive behavior live in the design system, not scattered across components.
Benefits
Rapid assembly - Developers focus on structure while the design system ensures consistency. No styling decisions needed during development.
Central maintenance - Design changes happen once and cascade everywhere. Rebrand your entire application by updating CSS variables.
Minimal footprint - Complete design system runs under 4.3KB, smaller than Tailwind's preflight CSS before adding any utilities.
Swappable design - Replace parts of the system or swap entire design languages without touching HTML structure. True separation enables design flexibility.
Team specialization - Designers control visual language through CSS. Developers control structure through HTML. Neither blocks the other.
Migration complete
After following this migration guide, you've transformed a complex Next.js application into a clean, standards-based architecture. The transformation is quite dramatic:
From 575MB to 1MB - Your project dependencies dropped from over 450+ NPM packages to a single global installation with zero external dependencies Configuration files reduced from 15+ to one central site.yaml
.
From mixed concerns to architectural clarity - Business logic lives in pure JavaScript modules. Content lives in Markdown files. Design lives in CSS. Structure lives in semantic HTML. Each layer works independently and can scale without affecting others.
From framework lock-in to web standards - Your forms use <form>
elements. Your buttons use <button>
elements. Your navigation uses <nav>
elements. The browser understands your application natively. No hydration, no virtual DOM, no framework abstractions between you and the platform.
From slow to instant feedback loop - Development builds take milliseconds instead of seconds. Hot reload works across all assets - frontend, backend, and database changes. The feedback loop becomes immediate.
What you gained
Maintainability - Clear separation of concerns makes the codebase easier to understand and modify. New team members can contribute immediately using skills they already have.
Performance - Order of magnitude improvements in bundle size, build speed, and runtime performance. Your entire application weighs less than a single React component.
Future-proofing - Web standards evolve slowly and deliberately. HTML, CSS, and JavaScript knowledge stays relevant for decades. Your investment compounds over time.
Team velocity - Designers control visual language through CSS without touching components. Content creators work independently through Markdown. Developers focus on business logic and structure. Nobody blocks anyone else.