nukejs
v0.0.21
Published
A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.
Downloads
124
Maintainers
Readme
NukeJS
A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.
npm create nuke@latestTable of Contents
- Overview
- Getting Started
- Project Structure
- Pages & Routing
- Layouts
- Client Components
- State Management
- API Routes
- Middleware
- Static Files
- useHtml() — Head Management
- Configuration
- Link Component & Navigation
- useRequest() — URL Params, Query & Headers
- Error Pages
- Building & Deploying
Overview
NukeJS gives you:
| Feature | Description |
|---|---|
| File-based routing | Pages in app/pages/, API in server/ |
| Server-side rendering | All pages rendered to HTML on the server |
| Partial hydration | Only "use client" components download JS |
| SPA navigation | Client-side page transitions after first load |
| Hot module replacement | Instant page updates during development |
| Zero config | Works out of the box; nuke.config.ts for overrides |
| Deploy anywhere | Node.js, Vercel, or Cloudflare — zero config |
The core idea
Most pages don't need JavaScript. NukeJS renders your entire React tree to HTML on the server, and only ships JavaScript for components explicitly marked "use client". Everything else stays server-only — no hydration cost, no JS bundle for static content.
// app/pages/index.tsx — Server component (zero JS sent to browser)
export default async function Home() {
const posts = await db.getPosts(); // runs on server only
return (
<main>
<h1>Blog</h1>
{posts.map(p => <PostCard key={p.id} post={p} />)}
<LikeButton postId={posts[0].id} /> {/* ← this one is interactive */}
</main>
);
}// app/components/LikeButton.tsx — Client component (JS downloaded)
"use client";
import { useState } from 'react';
export default function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}Getting Started
Prerequisites
- Node.js 20+
- React 19+
- esbuild (peer dependency)
Installation
npm create nuke@latestRunning the dev server
npm run devThe server starts on port 3000 by default (auto-increments if in use).
Project Structure
my-app/
├── app/
│ ├── pages/ # Page components (file-based routing)
│ │ ├── layout.tsx # Root layout (wraps every page)
│ │ ├── index.tsx # → /
│ │ ├── about.tsx # → /about
│ │ └── blog/
│ │ ├── layout.tsx # Blog section layout
│ │ ├── index.tsx # → /blog
│ │ └── [slug].tsx # → /blog/:slug
│ ├── components/ # Shared components (not routed)
│ └── public/ # Static files served at root (e.g. /favicon.ico)
├── server/ # API route handlers
│ ├── users/
│ │ ├── index.ts # → GET/POST /users
│ │ └── [id].ts # → GET/PUT/DELETE /users/:id
│ └── auth.ts # → /auth
├── middleware.ts # (optional) global request middleware
├── nuke.config.ts # (optional) configuration
└── package.jsonPages & Routing
Basic pages
Each .tsx file in app/pages/ maps to a URL route:
| File | URL |
|---|---|
| index.tsx | / |
| about.tsx | /about |
| blog/index.tsx | /blog |
| blog/[slug].tsx | /blog/:slug |
| docs/[...path].tsx | /docs/* (catch-all, required) |
| users/[[id]].tsx | /users or /users/42 (optional single segment) |
| files/[[...path]].tsx | /files or /files/* (optional catch-all) |
Page component
A page exports a default React component. It may be async (runs on the server).
// app/pages/blog/[slug].tsx
export default async function BlogPost({ slug }: { slug: string }) {
const post = await fetchPost(slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Route params are passed as props to the component.
Query string params
Query string parameters are automatically merged into the page component's props alongside route params. If a query param shares a name with a route param, the route param takes precedence.
// app/pages/search.tsx
// URL: /search?q=nuke&page=2
export default function Search({ q, page }: { q: string; page: string }) {
return <h1>Results for "{q}" — page {page}</h1>;
}// app/pages/blog/[slug].tsx
// URL: /blog/hello-world?preview=true
export default function BlogPost({ slug, preview }: { slug: string; preview?: string }) {
return <article data-preview={preview}>{slug}</article>;
}A query param that appears multiple times (e.g. ?tag=a&tag=b) is passed as a string[].
Catch-all routes
// app/pages/docs/[...path].tsx
export default function Docs({ path }: { path: string[] }) {
// path = ['getting-started', 'installation'] for /docs/getting-started/installation
return <DocViewer segments={path} />;
}Route specificity
When multiple routes could match a URL, the most specific one wins:
/users/profile → users/profile.tsx (static, wins)
/users/42 → users/[id].tsx (dynamic)
/users → users/[[id]].tsx (optional single, matches with no id)
/users/a/b/c → users/[...rest].tsx (catch-all)Specificity order, highest to lowest: static → [param] → [[param]] → [...catchAll] → [[...optionalCatchAll]].
Layouts
Place a layout.tsx alongside your pages to wrap a group of routes.
// app/pages/layout.tsx — Wraps every page
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<Nav />
<main>{children}</main>
<Footer />
</div>
);
}Layouts nest automatically. A page at blog/[slug].tsx gets wrapped by both layout.tsx (root) and blog/layout.tsx (blog section).
Title templates in layouts
// app/pages/layout.tsx
import { useHtml } from 'nukejs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
useHtml({ title: (prev) => `${prev} | Acme Corp` });
return <>{children}</>;
}
// app/pages/about.tsx
export default function About() {
useHtml({ title: 'About Us' });
// Final title: "About Us | Acme Corp"
return <h1>About</h1>;
}Client Components
Add "use client" as the very first line of any component file to make it a client component. NukeJS will:
- Bundle that file separately and serve it as
/__client-component/<id>.js - Render a
<span data-hydrate-id="…">placeholder in the server HTML - Hydrate the placeholder with React in the browser
"use client";
import { useState, useEffect } from 'react';
export default function Counter({ initial = 0 }: { initial?: number }) {
const [count, setCount] = useState(initial);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}Rules for client components
- The
"use client"directive must be the first non-comment line - The component must have a named default export (NukeJS uses the function name to match props during hydration)
- Props must be JSON-serializable (no functions, no class instances)
- React elements passed as props are supported (serialized and reconstructed)
Passing children to client components
Children and other React elements can be passed as props — NukeJS serializes them at render time:
// Server component
<Modal>
<p>This content is from the server</p>
</Modal>
// Modal is a "use client" component; its children are serialized as
// { __re: 'html', tag: 'p', props: { children: 'This content...' } }
// and reconstructed in the browser before mounting.State Management
NukeJS ships a lightweight built-in store for sharing state across client components. Because each "use client" component is hydrated into its own independent React root, React Context cannot cross component boundaries — the store solves this.
All store state lives in window.__nukeStores, so it is shared across every bundle on the page regardless of how many times a store module is evaluated.
createStore
import { createStore } from 'nukejs';
const counterStore = createStore('counter', { count: 0 });The first argument is a unique name used to key the store in the global registry. The second is the initial state. If two bundles call createStore with the same name, the first one wins and subsequent calls reuse the existing entry.
useStore
"use client"
import { useStore } from 'nukejs';
import { counterStore } from '../stores/counter';
export default function Counter() {
const { count } = useStore(counterStore);
return (
<div>
<p>{count}</p>
<button onClick={() => counterStore.setState(s => ({ count: s.count + 1 }))}>
Increment
</button>
</div>
);
}Pass an optional selector to avoid re-renders when unrelated parts of state change:
// Only re-renders when `count` changes, not on any other state update
const count = useStore(counterStore, s => s.count);Sharing state across components
Stores shine when two completely separate client components need to stay in sync. Define the store in its own file (no "use client" needed) and import it from both:
// app/stores/cart.ts
import { createStore } from 'nukejs';
export type CartItem = { id: string; name: string; price: number };
export const cartStore = createStore('cart', {
items: [] as CartItem[],
total: 0,
});// app/components/AddToCartButton.tsx
"use client"
import { cartStore, type CartItem } from '../stores/cart';
export default function AddToCartButton({ item }: { item: CartItem }) {
return (
<button onClick={() =>
cartStore.setState(s => ({
items: [...s.items, item],
total: s.total + item.price,
}))
}>
Add to cart
</button>
);
}// app/components/CartIcon.tsx
"use client"
import { useStore } from 'nukejs';
import { cartStore } from '../stores/cart';
export default function CartIcon() {
const { items, total } = useStore(cartStore);
return (
<span>
🛒 {items.length} items — ${total.toFixed(2)}
</span>
);
}// app/pages/shop.tsx — server component, no JS cost
import AddToCartButton from '../components/AddToCartButton';
import CartIcon from '../components/CartIcon';
export default function ShopPage() {
const item = { id: '1', name: 'Widget', price: 9.99 };
return (
<div>
<CartIcon />
<AddToCartButton item={item} />
</div>
);
}CartIcon updates instantly when the button is clicked — they are separate React roots with separate bundles, but they write and read through the same 'cart' entry in window.__nukeStores.
setState
Accepts a full replacement value or an updater function:
// Replace
cartStore.setState({ items: [], total: 0 });
// Updater — receives current state, returns next state
cartStore.setState(s => ({ ...s, total: s.total + 5 }));API reference
| | Description |
|---|---|
| createStore(name, initialState) | Creates or retrieves a named store |
| useStore(store) | Subscribes to the full state |
| useStore(store, selector) | Subscribes to a derived slice (re-renders only when slice changes) |
| store.getState() | Returns the current state snapshot |
| store.setState(updater) | Updates state and notifies all subscribers |
| store.subscribe(listener) | Registers a listener; returns an unsubscribe function |
Export named HTTP method handlers from .ts files in your server/ directory.
// server/users/index.ts
import type { ApiRequest, ApiResponse } from 'nukejs';
export async function GET(req: ApiRequest, res: ApiResponse) {
const users = await db.getUsers();
res.json(users);
}
export async function POST(req: ApiRequest, res: ApiResponse) {
const body = await req.json();
const user = await db.createUser(body);
res.json(user, 201);
}// server/users/[id].ts
export async function GET(req: ApiRequest, res: ApiResponse) {
const { id } = req.params as { id: string };
const user = await db.getUser(id);
if (!user) { res.json({ error: 'Not found' }, 404); return; }
res.json(user);
}
export async function DELETE(req: ApiRequest, res: ApiResponse) {
await db.deleteUser(req.params!.id as string);
res.status(204).end();
}Request object
| Property / Method | Type | Description |
|---|---|---|
| req.json<T>() | Promise<T> | Parse the request body as JSON (10 MB limit, prototype-pollution guard) |
| req.text() | Promise<string> | Read the request body as a UTF-8 string (10 MB limit) |
| req.buffer() | Promise<Buffer> | Read the request body as a raw Buffer — use this for binary or multipart data |
| req.params | Record<string, string \| string[]> | Dynamic route segments |
| req.query | Record<string, string> | URL search params |
| req.method | string | HTTP method |
| req.headers | IncomingHttpHeaders | Request headers |
Multipart / file uploads: body helpers do not parse
multipart/form-data. Pipereqdirectly into a multipart parser instead:import busboy from 'busboy'; const bb = busboy({ headers: req.headers }); req.pipe(bb);
Response object
| Method | Description |
|---|---|
| res.json(data, status?) | Send a JSON response (default status 200) |
| res.status(code) | Set status code and return res for chaining |
| res.setHeader(name, value) | Set a response header |
| res.end(body?) | Send raw response |
Middleware
Create middleware.ts in your project root to intercept every request before routing:
// middleware.ts
import type { IncomingMessage, ServerResponse } from 'http';
export default async function middleware(
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
// Logging
console.log(`${req.method} ${req.url}`);
// Auth guard
if (req.url?.startsWith('/admin') && !isAuthenticated(req)) {
res.statusCode = 401;
res.end('Unauthorized');
return; // End response to halt further processing
}
// Header injection (let request continue without ending it)
res.setHeader('X-Powered-By', 'nukejs');
}If res.end() (or res.json()) is called, NukeJS stops processing and does not handle the request through routing. If middleware returns without ending the response, the request continues to API routes or SSR.
Static Files
Place any file in app/public/ and it will be served directly at its path relative to that directory — no route file needed.
app/public/
├── favicon.ico → GET /favicon.ico
├── robots.txt → GET /robots.txt
├── logo.png → GET /logo.png
└── fonts/
└── inter.woff2 → GET /fonts/inter.woff2Every file type is served with the correct Content-Type automatically (images, fonts, CSS, video, audio, JSON, WASM, etc.).
Reference public files directly in your components:
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<link rel="icon" href="/favicon.ico" />
<img src="/logo.png" alt="Logo" />
{children}
</>
);
}Deployment behaviour
| Environment | How public files are served |
|---|---|
| nuke dev | Served by the built-in middleware before any API or SSR routing |
| nuke build (Node) | Copied to dist/static/ and served by the production HTTP server |
| nuke build (Vercel) | Copied to .vercel/output/static/ — served by Vercel's CDN, no function invocation |
| nuke build (Cloudflare) | Copied to .cloudflare/output/static/ — served by Cloudflare's CDN, no Worker invocation |
useHtml() — Head Management
The useHtml() hook works in both server components and client components to control the document <head>, <html> attributes, <body> attributes, and scripts injected at the end of <body>.
import { useHtml } from 'nukejs';
export default function Page() {
useHtml({
title: 'My Page',
meta: [
{ name: 'description', content: 'Page description' },
{ property: 'og:title', content: 'My Page' },
],
link: [
{ rel: 'canonical', href: 'https://example.com/page' },
{ rel: 'stylesheet', href: '/styles.css' },
],
htmlAttrs: { lang: 'en', class: 'dark' },
bodyAttrs: { class: 'page-home' },
});
return <main>...</main>;
}Title resolution order
When both a layout and a page call useHtml({ title }), they are resolved in this order:
Layout: useHtml({ title: (prev) => `${prev} | Site` })
Page: useHtml({ title: 'Home' })
Result: "Home | Site"The page title always serves as the base value; layout functions wrap it outward.
Script injection & position
The script option accepts an array of script tags. Each entry supports the standard attributes (src, type, async, defer, content for inline scripts, etc.) plus a position field:
| position | Where it's injected |
|---|---|
| 'head' (default) | Inside <head>, in the managed <!--n-head--> block |
| 'body' | End of <body>, just before </body>, in the <!--n-body-scripts--> block |
Use position: 'body' for third-party analytics and tracking scripts (Google Analytics, Hotjar, Intercom, etc.) that should load after page content is in the DOM and must not block rendering.
// app/pages/layout.tsx — Google Analytics on every page
import { useHtml } from 'nukejs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
useHtml({
script: [
// Load the gtag library — async so it doesn't block rendering
{
src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX',
async: true,
position: 'body',
},
// Inline initialisation — must follow the loader above
{
content: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
`,
position: 'body',
},
],
});
return <>{children}</>;
}Use position: 'head' (the default) for scripts that must run before first paint, such as theme detection to avoid flash-of-unstyled-content:
useHtml({
script: [
{
content: `
const theme = localStorage.getItem('theme') ?? 'light';
document.documentElement.classList.add(theme);
`,
// position defaults to 'head' — runs before the page renders
},
],
});Both head and body scripts are re-executed on every HMR update and SPA navigation so they always reflect the current page state.
Configuration
Create nuke.config.ts in your project root:
// nuke.config.ts
export default {
// Directory containing API route files (default: './server')
serverDir: './server',
// Port for the dev server (default: 3000, auto-increments if in use)
port: 3000,
// Logging verbosity
// false — silent (default)
// 'error' — errors only
// 'info' — startup messages + errors
// true — verbose (all debug output)
debug: false,
};Link Component & Navigation
Use the built-in <Link> component for client-side navigation (no full page reload):
import { Link } from 'nukejs';
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
}useRouter
"use client";
import { useRouter } from 'nukejs';
export default function SearchForm() {
const router = useRouter();
return (
<button onClick={() => router.push('/results?q=nuke')}>
Search
</button>
);
}useRequest() — URL Params, Query & Headers
useRequest() is a universal hook that exposes the current request's URL parameters, query string, and headers to any component — server or client, dev or production.
import { useRequest } from 'nukejs';
const { params, query, headers, pathname, url } = useRequest();| Field | Type | Description |
|---|---|---|
| url | string | Full URL with query string, e.g. /blog/hello?lang=en |
| pathname | string | Path only, e.g. /blog/hello |
| params | Record<string, string \| string[]> | Dynamic route segments |
| query | Record<string, string \| string[]> | Query-string params (multi-value keys become arrays) |
| headers | Record<string, string> | Request headers |
Where data comes from
| Environment | Source |
|---|---|
| Server (SSR) | Live IncomingMessage — all headers including cookie |
| Client (browser) | __n_data blob embedded in the page + window.location (reactive) |
On the client the hook is reactive: it re-reads on every SPA navigation so query, pathname, and params stay current without a page reload.
Security:
headerson the client never containscookie,authorization,proxy-authorization,set-cookie, orx-api-key. These are stripped before embedding in the HTML document so credentials cannot leak into cached or logged pages.
Reading route params and query string
// app/pages/blog/[slug].tsx
// URL: /blog/hello-world?tab=comments
import { useRequest } from 'nukejs';
export default function BlogPost() {
const { params, query } = useRequest();
const slug = params.slug as string;
const tab = (query.tab as string) ?? 'overview';
return (
<article>
<h1>{slug}</h1>
<p>Active tab: {tab}</p>
</article>
);
}Catch-all routes
// app/pages/docs/[...path].tsx
// URL: /docs/api/hooks → path = ['api', 'hooks']
import { useRequest } from 'nukejs';
export default function Docs() {
const { params } = useRequest();
const segments = params.path as string[];
return <nav>{segments.join(' › ')}</nav>;
}Reading headers in a server component
// app/pages/dashboard.tsx
import { useRequest } from 'nukejs';
export default async function Dashboard() {
const { headers } = useRequest();
// Forward the session cookie to an internal API call
const data = await fetch('http://localhost:3000/api/me', {
headers: { cookie: headers['cookie'] ?? '' },
}).then(r => r.json());
return <main>{data.name}</main>;
}Building useI18n on top
useRequest is designed as a primitive for higher-level hooks. Here is a complete useI18n implementation that works in both server and client components:
// app/hooks/useI18n.ts
import { useRequest } from 'nukejs';
const translations = {
en: { welcome: 'Welcome', signIn: 'Sign in' },
fr: { welcome: 'Bienvenue', signIn: 'Se connecter' },
de: { welcome: 'Willkommen', signIn: 'Anmelden' },
} as const;
type Locale = keyof typeof translations;
function detectLocale(
query: Record<string, string | string[]>,
acceptLanguage = '',
): Locale {
// ?lang=fr in the URL takes priority over the browser header
const fromQuery = query.lang as string | undefined;
if (fromQuery && fromQuery in translations) return fromQuery as Locale;
const fromHeader = acceptLanguage
.split(',')[0]?.split('-')[0]?.trim().toLowerCase();
if (fromHeader && fromHeader in translations) return fromHeader as Locale;
return 'en';
}
export function useI18n() {
const { query, headers } = useRequest();
const locale = detectLocale(query, headers['accept-language']);
return { t: translations[locale], locale };
}// app/pages/index.tsx
import { useI18n } from '../hooks/useI18n';
export default function Home() {
const { t } = useI18n();
return <h1>{t.welcome}</h1>;
}Changing ?lang=fr in the URL re-renders client components automatically.
Error Pages
NukeJS supports custom error pages for 404 Not Found and 500 Internal Server Error. Place them directly in app/pages/ — they are standard server components and support everything regular pages do: layouts, useHtml(), client components, and HMR in dev.
_404.tsx — Page Not Found
Rendered when no route matches the requested URL.
// app/pages/_404.tsx
import { useHtml } from 'nukejs';
import { Link } from 'nukejs';
export default function NotFound() {
useHtml({ title: 'Page Not Found' });
return (
<main>
<h1>404 — Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link href="/">Go home</Link>
</main>
);
}_500.tsx — Internal Server Error
Rendered when a page handler throws an unhandled error. The error is passed as optional props so you can display details in development.
// app/pages/_500.tsx
import { useHtml } from 'nukejs';
import { Link } from 'nukejs';
interface ErrorProps {
errorMessage?: string; // human-readable error description
errorStatus?: string; // HTTP status code if present on the thrown error
errorStack?: string; // stack trace — only populated in development
}
export default function ServerError({ errorMessage, errorStack }: ErrorProps) {
useHtml({ title: 'Something went wrong' });
return (
<main>
<h1>500 — Server Error</h1>
<p>Something went wrong on our end. Please try again.</p>
{errorMessage && <p><strong>{errorMessage}</strong></p>}
{errorStack && <pre>{errorStack}</pre>}
<Link href="/">Go home</Link>
</main>
);
}Server errors
Any unhandled throw inside a server page component (including async data fetching) routes to _500.tsx. The error message and stack trace are forwarded as props automatically.
// app/pages/dashboard.tsx
export default async function Dashboard() {
const data = await fetchData(); // throws → _500.tsx is rendered
return <main>{data.name}</main>;
}You can also attach a status property to a thrown error to control the HTTP status code sent with the response:
export default async function Post({ id }: { id: string }) {
const post = await db.getPost(id);
if (!post) {
const err = new Error('Post not found');
(err as any).status = 404;
throw err; // _500.tsx receives errorMessage="Post not found", errorStatus="404"
}
return <article>{post.title}</article>;
}Client errors
Unhandled errors in client components and async code are automatically caught and routed to _500.tsx via an in-place SPA navigation — no full page reload. Three mechanisms cover all cases:
- React Error Boundary — wraps every hydrated
"use client"component; catches render and lifecycle errors window.onerror— catches synchronous throws in event handlers and other non-React codewindow.onunhandledrejection— catches unhandledPromiserejections fromasyncfunctions
// app/components/FaultyButton.tsx
"use client";
export default function FaultyButton() {
const handleClick = () => {
throw new Error('Something broke!'); // caught by window.onerror → _500.tsx
};
return <button onClick={handleClick}>Click me</button>;
}// app/components/FaultyFetch.tsx
"use client";
import { useEffect } from 'react';
export default function FaultyFetch() {
useEffect(() => {
// Unhandled rejection caught by window.onunhandledrejection → _500.tsx
fetch('/api/broken').then(res => {
if (!res.ok) throw new Error(`API error ${res.status}`);
});
}, []);
return <div>Loading...</div>;
}The _500.tsx page receives errorMessage and errorStack props from client errors just like server errors, so a single error page handles both origins consistently.
Behaviour
| Scenario | Without _500.tsx | With _500.tsx |
|---|---|---|
| Server page throws | Plain-text Internal Server Error (500) | _500.tsx rendered with error props |
| Client component render error | React crashes the component subtree | _500.tsx rendered in-place, no reload |
| Unhandled event handler throw | Browser console error only | _500.tsx rendered in-place, no reload |
| Unhandled promise rejection | Browser console error only | _500.tsx rendered in-place, no reload |
| Unknown URL | Plain-text Page not found (404) | _404.tsx rendered with 404 status |
| <Link> to unknown URL | Full page reload | In-place SPA navigation, no reload |
| HMR save of _404.tsx / _500.tsx | — | Current page re-fetches immediately |
Notes
- Error pages are excluded from routing —
/_404and/_500are not reachable as URLs. - They participate in the root
layout.tsxlike any other page. - Both are fully bundled into the production output (Node.js and Vercel) — no runtime file-system access required.
- The correct HTTP status code (404 or 500) is always sent in the response.
errorStackis only populated in development (NODE_ENV !== 'production').
Building & Deploying
Node.js server
npm run build # builds to dist/
node dist/index.mjs # starts the production serverThe build output:
dist/
├── api/ # Bundled API route handlers (.mjs)
├── pages/ # Bundled page handlers (.mjs)
├── static/
│ ├── __n.js # NukeJS client runtime (React + NukeJS bundled together)
│ ├── __client-component/ # Bundled "use client" component files
│ └── <app/public files> # Copied from app/public/ at build time
├── manifest.json # Route dispatch table
└── index.mjs # HTTP server entry pointVercel
Just import the code from GitHub. NukeJS detects the Vercel environment automatically and builds the right output — no configuration needed.
Cloudflare Workers & Pages
Just import the code from GitHub. NukeJS detects the Cloudflare environment automatically and builds the right output — no configuration needed.
The build output goes to .cloudflare/output/:
.cloudflare/output/
├── _worker.mjs # Single ESM Cloudflare Worker (all routes bundled)
└── static/
├── __n.js # NukeJS client runtime
├── __client-component/ # Bundled "use client" component files
└── <app/public files> # Copied from app/public/ at build timeStatic files in static/ are served directly by Cloudflare's CDN — the Worker is only invoked for pages and API routes.
Cloudflare Pages (recommended): Set your build output directory to
.cloudflare/outputin the Pages dashboard. Static assets are served via CDN automatically through theASSETSbinding.
Cloudflare Workers: Use
wrangler deployas your deploy command. Static assets are inlined into the worker bundle at build time — no separate CDN step required.
Environment variables
| Variable | Description |
|---|---|
| ENVIRONMENT=production | Disables HMR and file watching |
| PORT | Port for the production server |
License
MIT

