Skip to main content
Next.js is a powerful React framework that offers server-side rendering, static site generation, file-based routing, and an exceptional developer experience. When using Next.js with Mizu, you’ll leverage Next.js’s static export mode to generate a fully static site, while Mizu handles your backend API and serves the static files.

Why Next.js with Mizu?

Next.js brings several advantages to your Mizu applications: File-based routing - Define routes by creating files in the app/ directory. No need to configure a router manually. React Server Components - Write components that run during build time, reducing client-side JavaScript. App Router - Modern routing with layouts, loading states, error boundaries, and parallel routes. Developer Experience - Fast Refresh, TypeScript support, and excellent error messages out of the box. Optimizations - Automatic code splitting, image optimization (with configuration), and font optimization.

Next.js with Mizu vs Standalone Next.js

FeatureNext.js + MizuStandalone Next.js
HostingSingle Go binaryNode.js server or Vercel
BackendGo handlersNext.js API routes
DeploymentAny server with GoNode.js required
SSR❌ No (static export)βœ… Yes
Static Exportβœ… Yesβœ… Yes
API RoutesGo backendJavaScript
Performance⚑ Very fast (Go)⚑ Fast (Node.js)
Type SafetyBackend: Go, Frontend: TSFull-stack TypeScript
When to use Next.js with Mizu:
  • You want Next.js features (App Router, RSC, file-based routing)
  • You prefer Go for backend APIs
  • You want a single binary deployment
  • You’re building a static-exportable site
When to use standalone Next.js:
  • You need full SSR (server-side rendering at request time)
  • You want Next.js API routes
  • You prefer Node.js for everything
  • Your team is all JavaScript/TypeScript

How Static Export Works

Next.js’s static export generates HTML files at build time:
Build Process
    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Next.js analyzes routes     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Renders pages to HTML       β”‚
β”‚ - Server Components run     β”‚
β”‚ - Client Components bundle  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Outputs static files        β”‚
β”‚ - build/                    β”‚
β”‚   β”œβ”€β”€ index.html           β”‚
β”‚   β”œβ”€β”€ about.html           β”‚
β”‚   └── _next/               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
What happens at build time:
  1. Next.js crawls all routes starting from page files
  2. Server Components execute and generate HTML
  3. Client Components are bundled into JavaScript
  4. Static HTML files are created for each route
  5. Assets are optimized and fingerprinted
What happens at runtime:
  1. Mizu serves the pre-built HTML files
  2. Browser loads HTML + JavaScript
  3. React hydrates the page
  4. Client-side navigation takes over
  5. API calls go to Mizu backend (Go)

Quick Start

Create a new Next.js project with the CLI:
mizu new ./my-nextjs-app --template frontend/next
cd my-nextjs-app
make dev
Visit http://localhost:3000 to see your app!

Project Structure

my-nextjs-app/
β”œβ”€β”€ cmd/
β”‚   └── server/
β”‚       └── main.go              # Go entry point
β”œβ”€β”€ app/
β”‚   └── server/
β”‚       β”œβ”€β”€ app.go               # Mizu app configuration
β”‚       β”œβ”€β”€ config.go            # Server configuration
β”‚       └── routes.go            # API routes (Go)
β”œβ”€β”€ frontend/                      # Next.js application
β”‚   β”œβ”€β”€ app/                     # App Router (Next.js 13+)
β”‚   β”‚   β”œβ”€β”€ layout.tsx           # Root layout
β”‚   β”‚   β”œβ”€β”€ page.tsx             # Home page (/)
β”‚   β”‚   β”œβ”€β”€ about/
β”‚   β”‚   β”‚   └── page.tsx         # About page (/about)
β”‚   β”‚   └── users/
β”‚   β”‚       β”œβ”€β”€ page.tsx         # Users list (/users)
β”‚   β”‚       └── [id]/
β”‚   β”‚           └── page.tsx     # User detail (/users/123)
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ Header.tsx
β”‚   β”‚   └── Footer.tsx
β”‚   β”œβ”€β”€ public/                  # Static assets
β”‚   β”‚   └── images/
β”‚   β”œβ”€β”€ next.config.mjs          # Next.js configuration
β”‚   β”œβ”€β”€ package.json
β”‚   └── tsconfig.json
β”œβ”€β”€ build/                       # Built files (after npm run build)
β”œβ”€β”€ go.mod
└── Makefile

Configuration

Next.js Configuration

The key to using Next.js with Mizu is configuring static export:

frontend/next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable static export - this is crucial!
  output: 'export',

  // Output to 'build' directory (must match Mizu config)
  distDir: '../build',

  // Disable image optimization for static export
  images: {
    unoptimized: true
  },

  // Optional: set base path if serving from subdirectory
  // basePath: '/app',

  // Optional: trailing slash behavior
  // trailingSlash: true,
}

export default nextConfig
Configuration explained:
  • output: β€˜export’ - Tells Next.js to generate static HTML files instead of running a Node.js server
  • distDir: ’../build’ - Outputs files to build/ (one level up from frontend/)
  • images.unoptimized - Disables Next.js Image Optimization API (requires Node.js server)

Backend Configuration

app/server/app.go

package server

import (
    "embed"
    "io/fs"

    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/frontend"
)

// Embed the Next.js build output
//go:embed all:../../build
var buildFS embed.FS

func New(cfg *Config) *mizu.App {
    app := mizu.New()

    // API routes come first
    app.Get("/api/users", handleUsers)
    app.Post("/api/users", createUser)
    app.Get("/api/users/{id}", getUser)

    // Extract 'build' subdirectory from embedded FS
    build, _ := fs.Sub(buildFS, "build")

    // Frontend middleware (handles all non-API routes)
    app.Use(frontend.WithOptions(frontend.Options{
        Mode:        frontend.ModeAuto,       // Auto-detect dev/prod
        FS:          build,                    // Embedded build files
        Root:        "./build",                // Fallback to filesystem
        DevServer:   "http://localhost:" + cfg.DevPort,  // Next.js dev server
        IgnorePaths: []string{"/api"},        // Don't proxy /api to Next.js
    }))

    return app
}

app/server/routes.go

package server

import "github.com/go-mizu/mizu"

func setupRoutes(app *mizu.App) {
    // User API
    app.Get("/api/users", handleUsers)
    app.Post("/api/users", createUser)
    app.Get("/api/users/{id}", getUser)
    app.Put("/api/users/{id}", updateUser)
    app.Delete("/api/users/{id}", deleteUser)
}

func handleUsers(c *mizu.Ctx) error {
    users := []map[string]any{
        {"id": 1, "name": "Alice", "email": "[email protected]"},
        {"id": 2, "name": "Bob", "email": "[email protected]"},
        {"id": 3, "name": "Charlie", "email": "[email protected]"},
    }
    return c.JSON(200, users)
}

func createUser(c *mizu.Ctx) error {
    var user map[string]any
    if err := c.BodyJSON(&user); err != nil {
        return c.JSON(400, map[string]string{"error": "Invalid JSON"})
    }

    // Add ID (in real app, use database)
    user["id"] = 4

    return c.JSON(201, user)
}

func getUser(c *mizu.Ctx) error {
    id := c.Param("id")
    user := map[string]any{
        "id":    id,
        "name":  "User " + id,
        "email": "user" + id + "@example.com",
    }
    return c.JSON(200, user)
}

File-Based Routing

Next.js uses file-based routing in the app/ directory. Each folder represents a route segment, and special files define the UI.

Route Files

FilePurposeRequired
layout.tsxShared UI for a segment and its childrenYes (root)
page.tsxUnique UI for a route, makes it publicly accessibleYes
loading.tsxLoading UI (Suspense boundary)No
error.tsxError UI (Error boundary)No
not-found.tsx404 UINo

Example Routes

app/
β”œβ”€β”€ layout.tsx           β†’ Root layout (wraps everything)
β”œβ”€β”€ page.tsx             β†’ Home page: /
β”œβ”€β”€ about/
β”‚   └── page.tsx         β†’ About: /about
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ layout.tsx       β†’ Blog layout (wraps all blog pages)
β”‚   β”œβ”€β”€ page.tsx         β†’ Blog home: /blog
β”‚   └── [slug]/
β”‚       └── page.tsx     β†’ Blog post: /blog/hello-world
└── users/
    β”œβ”€β”€ page.tsx         β†’ Users list: /users
    └── [id]/
        └── page.tsx     β†’ User detail: /users/123

Root Layout

The root layout wraps your entire application:

frontend/app/layout.tsx

import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'My Mizu App',
  description: 'Built with Next.js and Mizu',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <nav className="navbar">
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href="/users">Users</a>
        </nav>

        <main className="container">
          {children}
        </main>

        <footer className="footer">
          <p>Β© 2025 My Mizu App</p>
        </footer>
      </body>
    </html>
  )
}

Dynamic Routes

Use brackets for dynamic segments:

frontend/app/users/[id]/page.tsx

'use client'

import { useEffect, useState } from 'react'

interface User {
  id: string
  name: string
  email: string
}

export default function UserPage({ params }: { params: { id: string } }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    fetch(`/api/users/${params.id}`)
      .then(res => {
        if (!res.ok) throw new Error('User not found')
        return res.json()
      })
      .then(setUser)
      .catch(err => setError(err.message))
      .finally(() => setLoading(false))
  }, [params.id])

  if (loading) return <div>Loading user...</div>
  if (error) return <div className="error">Error: {error}</div>
  if (!user) return <div>User not found</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>ID: {user.id}</p>
    </div>
  )
}

Server Components vs Client Components

Next.js 13+ introduces React Server Components (RSC). Understanding the difference is crucial.

Server Components (Default)

Components are Server Components by default. They run at build time (in static export mode):
// app/page.tsx - Server Component (no 'use client')

export default function Home() {
  // This runs at BUILD time
  const buildTime = new Date().toISOString()

  return (
    <div>
      <h1>Welcome</h1>
      <p>Built at: {buildTime}</p>
    </div>
  )
}
Server Component benefits:
  • Zero JavaScript sent to browser for the component logic
  • Can read files, query databases (at build time)
  • Better performance - less client-side JavaScript
Server Component limitations:
  • Cannot use hooks (useState, useEffect, etc.)
  • Cannot use browser APIs
  • Cannot handle user interactions directly

Client Components

Add 'use client' directive for interactive components:
'use client'

import { useState } from 'react'

export default function Counter() {
  // This runs in the BROWSER
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  )
}
Client Component benefits:
  • Can use hooks and state
  • Can handle user interactions
  • Can use browser APIs
  • Can use useEffect for side effects
Client Component limitations:
  • JavaScript bundle sent to browser
  • Cannot use server-only features (fs, database)

Composition Pattern

Compose Server and Client Components:
// app/page.tsx - Server Component
import Counter from './Counter'  // Client Component

export default function Home() {
  const data = { message: "Hello from server" }

  return (
    <div>
      <h1>Home Page</h1>
      <p>{data.message}</p>
      {/* Client component nested in server component */}
      <Counter />
    </div>
  )
}
// app/Counter.tsx - Client Component
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

Data Fetching

Fetching in Client Components

Use useEffect and fetch:
'use client'

import { useEffect, useState } from 'react'

interface User {
  id: number
  name: string
  email: string
}

export default function Users() {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers)
      .finally(() => setLoading(false))
  }, [])

  if (loading) return <div>Loading...</div>

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Using React Query

Install React Query for better data fetching:
cd frontend
npm install @tanstack/react-query
Setup provider:
// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}
// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  )
}
Use in components:
'use client'

import { useQuery } from '@tanstack/react-query'

export default function Users() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json())
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}

Metadata and SEO

Next.js makes SEO easy with metadata:

Static Metadata

// app/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Home - My App',
  description: 'Welcome to my app built with Mizu and Next.js',
  keywords: ['Next.js', 'Mizu', 'Go', 'React'],
  openGraph: {
    title: 'Home - My App',
    description: 'Welcome to my app',
    images: ['/og-image.png'],
  },
}

export default function Home() {
  return <h1>Home</h1>
}

Dynamic Metadata

// app/users/[id]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    title: `User ${params.id} - My App`,
    description: `Profile page for user ${params.id}`,
  }
}

export default function UserPage({ params }) {
  return <h1>User {params.id}</h1>
}

Image Optimization

Next.js’s Image component requires a Node.js server for optimization. In static export mode, you have options:

Option 1: Use Regular <img> Tags

export default function Logo() {
  return <img src="/logo.png" alt="Logo" width={200} height={100} />
}

Option 2: Use next-image-export-optimizer

This package optimizes images at build time:
npm install next-image-export-optimizer
// next.config.mjs
const nextConfig = {
  output: 'export',
  images: {
    loader: 'custom',
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
  },
  env: {
    nextImageExportOptimizer_imageFolderPath: "public/images",
    nextImageExportOptimizer_exportFolderPath: "build",
    nextImageExportOptimizer_quality: "75",
  },
}
import ExportedImage from 'next-image-export-optimizer'

export default function Logo() {
  return (
    <ExportedImage
      src="/images/logo.png"
      alt="Logo"
      width={200}
      height={100}
    />
  )
}

Option 3: Optimize Manually

Use tools like sharp or ImageMagick to pre-optimize images before adding them to public/.

Development Workflow

Starting Development

# Option 1: Use Makefile (recommended)
make dev

# Option 2: Manual (two terminals)

# Terminal 1: Next.js dev server
cd frontend
npm run dev   # Starts on http://localhost:3001

# Terminal 2: Mizu server
go run cmd/server/main.go  # Starts on http://localhost:3000
How it works in development:
  1. Next.js runs its dev server on port 3001
  2. Mizu runs on port 3000
  3. Requests to Mizu’s port 3000 get proxied to Next.js (except /api)
  4. You visit http://localhost:3000 in your browser
  5. Hot reload works through Mizu’s proxy

Making Changes

Frontend changes:
  • Edit any file in frontend/
  • Next.js Fast Refresh updates browser instantly
  • No restart needed
Backend changes:
  • Edit Go files
  • Restart Mizu server
  • Or use air for auto-reload:
# Install air
go install github.com/cosmtrek/air@latest

# Run with auto-reload
air

Building for Production

Build the complete application:
make build
This runs:
  1. cd frontend && npm run build - Next.js static export
  2. go build -o bin/server cmd/server/main.go - Go binary with embedded frontend

Build Output

build/
β”œβ”€β”€ index.html              # Home page
β”œβ”€β”€ about.html              # About page
β”œβ”€β”€ users.html              # Users list
β”œβ”€β”€ users/
β”‚   └── 123.html           # Example user page
β”œβ”€β”€ _next/
β”‚   β”œβ”€β”€ static/
β”‚   β”‚   └── chunks/        # JavaScript bundles
β”‚   └── ...
└── images/                # Static assets

Running in Production

MIZU_ENV=production ./bin/server
The binary contains:
  • Mizu web server
  • Your Go API handlers
  • Entire Next.js build embedded

Limitations with Static Export

When using output: 'export', some Next.js features are unavailable:
FeatureAvailableNotes
File-based routingβœ… YesWorks perfectly
Server Componentsβœ… YesRun at build time
Client Componentsβœ… YesFull support
Dynamic routesβœ… YesUsing [brackets]
Layoutsβœ… YesFull support
Loading UIβœ… YesFull support
Error boundariesβœ… YesFull support
API Routes❌ NoUse Mizu Go handlers instead
Server-Side Rendering❌ NoOnly static export
getServerSideProps❌ NoUse client-side fetching
revalidate❌ NoNo ISR in static export
Image Optimization API❌ NoUse alternatives
Middleware❌ NoUse Mizu middleware
Server Actions❌ NoUse Mizu API routes

Working Around Limitations

Instead of API Routes:
// ❌ app/api/users/route.ts - Doesn't work in static export
export async function GET() {
  return Response.json([])
}

// βœ… Use Mizu Go handlers instead
// app/server/routes.go
func handleUsers(c *mizu.Ctx) error {
    return c.JSON(200, users)
}
Instead of SSR:
// ❌ Using getServerSideProps - Doesn't work
export async function getServerSideProps() {
  const data = await fetch('...')
  return { props: { data } }
}

// βœ… Client-side fetching instead
'use client'
export default function Page() {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetch('/api/data').then(r => r.json()).then(setData)
  }, [])
}

Troubleshooting

Build Errors: β€œoutput: export” Issues

Error:
Error: Page "/api/users" is incompatible with "output: export"
Cause: You have API routes in app/api/ Solution: Remove app/api/ directory. Use Mizu Go handlers instead.

Hydration Mismatch Errors

Error:
Warning: Text content did not match. Server: "..." Client: "..."
Cause: Server-rendered HTML doesn’t match client-rendered HTML Solution: Ensure Server Components don’t use time-dependent values:
// ❌ Bad - time changes between build and runtime
export default function Page() {
  return <p>{new Date().toString()}</p>
}

// βœ… Good - use client component for dynamic content
'use client'
export default function Page() {
  const [time, setTime] = useState(new Date())
  useEffect(() => {
    const timer = setInterval(() => setTime(new Date()), 1000)
    return () => clearInterval(timer)
  }, [])
  return <p>{time.toString()}</p>
}

Images Not Loading

Error: Images show broken icon Cause: Next.js Image component requires server Solution: Use one of the image optimization alternatives mentioned above.

Can’t Find Module

Error:
Module not found: Can't resolve '@/components/Header'
Cause: Path alias not configured Solution: Check tsconfig.json:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./app/*"]
    }
  }
}

Real-World Example: Blog with Comments

Here’s a complete example showing Server Components, Client Components, and API integration:

Backend

// app/server/routes.go
func setupRoutes(app *mizu.App) {
    app.Get("/api/posts", handlePosts)
    app.Get("/api/posts/{id}", getPost)
    app.Get("/api/posts/{id}/comments", getComments)
    app.Post("/api/posts/{id}/comments", createComment)
}

func handlePosts(c *mizu.Ctx) error {
    posts := []map[string]any{
        {"id": 1, "title": "First Post", "slug": "first-post"},
        {"id": 2, "title": "Second Post", "slug": "second-post"},
    }
    return c.JSON(200, posts)
}

func getPost(c *mizu.Ctx) error {
    id := c.Param("id")
    post := map[string]any{
        "id":      id,
        "title":   "Post " + id,
        "content": "This is the content of post " + id,
    }
    return c.JSON(200, post)
}

func getComments(c *mizu.Ctx) error {
    // Return comments for post
    comments := []map[string]any{
        {"id": 1, "author": "Alice", "text": "Great post!"},
        {"id": 2, "author": "Bob", "text": "Thanks for sharing"},
    }
    return c.JSON(200, comments)
}

Frontend

// app/blog/[slug]/page.tsx
import Comments from './Comments'

export default function BlogPost({ params }: { params: { slug: string } }) {
  // Server Component - runs at build time

  return (
    <article>
      <h1>Post: {params.slug}</h1>
      <p>This is a blog post about {params.slug}</p>

      {/* Client Component for interactivity */}
      <Comments slug={params.slug} />
    </article>
  )
}
// app/blog/[slug]/Comments.tsx
'use client'

import { useState, useEffect } from 'react'

export default function Comments({ slug }: { slug: string }) {
  const [comments, setComments] = useState([])
  const [newComment, setNewComment] = useState('')
  const [author, setAuthor] = useState('')

  useEffect(() => {
    fetch(`/api/posts/${slug}/comments`)
      .then(r => r.json())
      .then(setComments)
  }, [slug])

  const handleSubmit = async (e) => {
    e.preventDefault()

    const response = await fetch(`/api/posts/${slug}/comments`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ author, text: newComment }),
    })

    if (response.ok) {
      const comment = await response.json()
      setComments([...comments, comment])
      setNewComment('')
      setAuthor('')
    }
  }

  return (
    <div>
      <h2>Comments</h2>

      <ul>
        {comments.map((c: any) => (
          <li key={c.id}>
            <strong>{c.author}:</strong> {c.text}
          </li>
        ))}
      </ul>

      <form onSubmit={handleSubmit}>
        <input
          value={author}
          onChange={e => setAuthor(e.target.value)}
          placeholder="Your name"
          required
        />
        <textarea
          value={newComment}
          onChange={e => setNewComment(e.target.value)}
          placeholder="Your comment"
          required
        />
        <button type="submit">Post Comment</button>
      </form>
    </div>
  )
}

When to Choose Next.js

Choose Next.js When:

βœ… You want file-based routing without manual configuration βœ… You’re building a primarily static site (blog, marketing site, docs) βœ… You want to use React Server Components βœ… Your team knows React and wants enhanced DX βœ… You want automatic code splitting and optimizations βœ… You need good SEO with metadata support

Choose Vanilla React When:

βœ… You want complete control over the setup βœ… You don’t need file-based routing βœ… You prefer a simpler build process βœ… Your app is highly dynamic (not suitable for static export) βœ… Bundle size needs to be minimal βœ… You don’t need Server Components

Next Steps