npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Readme

NukeJS Banner

NukeJS npm website

A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.

npm create nuke@latest

Table of Contents

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@latest

Running the dev server

npm run dev

The 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.json

Pages & 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:

  1. Bundle that file separately and serve it as /__client-component/<id>.js
  2. Render a <span data-hydrate-id="…"> placeholder in the server HTML
  3. 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. Pipe req directly 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.woff2

Every 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: headers on the client never contains cookie, authorization, proxy-authorization, set-cookie, or x-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 code
  • window.onunhandledrejection — catches unhandled Promise rejections from async functions
// 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/_404 and /_500 are not reachable as URLs.
  • They participate in the root layout.tsx like 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.
  • errorStack is 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 server

The 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 point

Vercel

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 time

Static 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/output in the Pages dashboard. Static assets are served via CDN automatically through the ASSETS binding.

Cloudflare Workers: Use wrangler deploy as 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