Coming from Next.js

No file-based routing, no special directory structure, no framework-specific patterns to unlearn. Just a React component, a config file, and a function that runs on the server.

Philosophy

Next.js owns your routing, rendering mode, image pipeline, and deployment target. hadars does one thing: renders a React component on the server and hydrates it on the client. Your router, your CSS, your deploy target.

AspectNext.jshadars
RoutingFile-based (pages/ or app/)Bring your own (react-router, TanStack Router, …)
Data fetchinggetServerSideProps / Server ComponentsgetInitProps + useServerData
Rendering modesSSR, SSG, ISR, PPR, edgeSSR + static export (hadars export static)
Head managementnext/head / metadata API<HadarsHead> anywhere in the tree
Static filespublic/ directorystatic/ directory at project root — served automatically
Image handlingnext/image (automatic)Plain <img> (your CDN, your rules)
CSSCSS Modules, built-inAnything (Tailwind, Emotion, plain CSS, …)
Confignext.config.js (large API)hadars.config.ts (~10 options)
Bundle toolSWC + Webpack/Turbopackrspack + SWC
RuntimeNode.js / edge runtimesNode.js, Bun, Deno
Deploy targetVercel / self-hostedAny HTTP server, AWS Lambda, or Cloudflare Workers

Project structure

Next.js projects are organised around the framework's conventions. hadars has no opinion — the only required file is your entry component.

Typical project layout

Next.js
my-app/
├── app/                  # or pages/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── about/page.tsx
│   └── blog/[slug]/page.tsx
├── public/
├── next.config.js
└── package.json
hadars
my-app/
├── src/
│   ├── App.tsx           # single entry point
│   ├── pages/            # your choice
│   │   ├── Home.tsx
│   │   ├── About.tsx
│   │   └── Blog.tsx
│   └── components/
├── static/               # served as-is (≈ public/)
│   └── robots.txt
├── hadars.config.ts
└── package.json
hadars has no pages/ or app/ convention. You can organise files however you like — hadars only cares about the single entry component you point to in hadars.config.ts.

Routing

Next.js routes are created by adding files to a directory. hadars doesn't touch routing at all — you wire up your own router in your entry component.

Defining routes

Next.js
// app/page.tsx → "/"
export default function Home() {
    return <h1>Home</h1>;
}

// app/about/page.tsx → "/about"
export default function About() {
    return <h1>About</h1>;
}

// app/blog/[slug]/page.tsx → "/blog/:slug"
export default function Blog({ params }) {
    return <h1>{params.slug}</h1>;
}
hadars
// src/App.tsx — routes live in your component
import { BrowserRouter, StaticRouter,
         Routes, Route } from 'react-router-dom';

const App: HadarsApp<Props> = ({ location }) => {
    const routes = (
        <Routes>
            <Route path="/"            element={<Home />} />
            <Route path="/about"       element={<About />} />
            <Route path="/blog/:slug"  element={<Blog />} />
        </Routes>
    );
    if (typeof window === 'undefined')
        return <StaticRouter location={location}>{routes}</StaticRouter>;
    return <BrowserRouter>{routes}</BrowserRouter>;
};
Because hadars passes the current URL as a location prop, any SSR-compatible router will work: react-router, TanStack Router, wouter, etc.

Data fetching

Next.js has several patterns depending on the rendering mode. hadars has three: getInitProps for request-level data, useServerData for component-level async data, and useGraphQL for component-level GraphQL queries (when source plugins or a custom executor are configured).

Fetching data for a page (App Router)

Next.js
// app/profile/page.tsx
async function getUser(id: string) {
    return fetch(`/api/users/${id}`).then(r => r.json());
}

// Server Component — runs on the server
export default async function Profile() {
    const user = await getUser('123');
    return <h1>Hello {user.name}</h1>;
}
hadars
// src/pages/Profile.tsx
import { useServerData } from 'hadars';

// Runs inside the component during SSR.
// On the client, reads from the hydration cache.
const Profile: React.FC<{ userId: string }> = ({ userId }) => {
    const user = useServerData(() =>
        fetch(`/api/users/${userId}`).then(r => r.json())
    );
    if (!user) return null;
    return <h1>Hello {user.name}</h1>;
};

Fetching data for the whole request (Pages Router equivalent)

Next.js
// pages/dashboard.tsx
export async function getServerSideProps(ctx) {
    const user = await db.getUser(ctx.req.cookies.session);
    return { props: { user } };
}

export default function Dashboard({ user }) {
    return <h1>Hello {user.name}</h1>;
}
hadars
// src/App.tsx (or wherever your route renders)
export const getInitProps = async (req: HadarsRequest) => {
    const user = await db.getUser(req.cookies.session);
    return { user };
};

const App: HadarsApp<{ user: User }> = ({ user }) => (
    <h1>Hello {user.name}</h1>
);
useServerData batches all calls within one render into a single JSON request during client-side navigation — you never make N requests for N data hooks.

Head management

Setting the page title and meta tags

Next.js
// App Router — metadata export
export const metadata = {
    title: 'My page',
    description: 'A great page',
    openGraph: { title: 'My page' },
};

// — or — Pages Router
import Head from 'next/head';

export default function Page() {
    return (
        <>
            <Head>
                <title>My page</title>
                <meta name="description" content="A great page" />
            </Head>
            <main>...</main>
        </>
    );
}
hadars
import { HadarsHead } from 'hadars';

// Works in any component, any depth in the tree.
// Server: injects into <head> before sending HTML.
// Client: upserts — no duplicates on navigation.
const Page = () => (
    <>
        <HadarsHead status={200}>
            <title>My page</title>
            <meta name="description" content="A great page" />
            <meta property="og:title" content="My page" />
        </HadarsHead>
        <main>...</main>
    </>
);
Unlike next/head, HadarsHead deduplicates tags by their natural key (meta name, property, link rel+href, etc.) — navigating between pages updates existing tags in place rather than appending duplicates.

HTTP status codes

Returning a 404

Next.js
// App Router
import { notFound } from 'next/navigation';

export default function Page({ params }) {
    const post = getPost(params.slug);
    if (!post) notFound();      // throws
    return <article>{post.title}</article>;
}

// Pages Router
export async function getServerSideProps() {
    return { notFound: true };
}
hadars
import { HadarsHead } from 'hadars';

// Set status on any HadarsHead in the tree.
// The last one with a status prop wins.
const NotFound: React.FC = () => (
    <>
        <HadarsHead status={404}>
            <title>Not found</title>
        </HadarsHead>
        <h1>Page not found</h1>
    </>
);

// In your router:
<Route path="*" element={<NotFound />} />

API routes

Next.js has built-in API routes. hadars uses a fetch hook in the config for custom server-side handling — or you can proxy to a separate backend.

Handling a custom server endpoint

Next.js
// app/api/hello/route.ts
export async function GET(req: Request) {
    return Response.json({ message: 'hello' });
}
hadars
// hadars.config.ts
const config: HadarsOptions = {
    entry: 'src/App.tsx',

    fetch: async (req) => {
        if (req.pathname === '/api/hello') {
            return Response.json({ message: 'hello' });
        }
        // return undefined → falls through to SSR
    },
};
The fetch hook intercepts every request before SSR. Return a Response to short-circuit; return nothing to let hadars render the page normally. For larger APIs, use proxy to forward requests to a dedicated service.

Code splitting

Lazy-loading a heavy component

Next.js
import dynamic from 'next/dynamic';

// Next.js wraps React.lazy with SSR control
const HeavyChart = dynamic(
    () => import('./HeavyChart'),
    { ssr: false }   // skip SSR for this component
);
hadars
import { loadModule } from 'hadars';

// loadModule is React.lazy-compatible.
// Browser: dynamic import() → separate JS chunk.
// Server: synchronous require() → included in SSR.
const HeavyChart = React.lazy(
    () => loadModule('./HeavyChart')
);
hadars doesn't have an ssr: false option — all split modules are included in the SSR bundle. If you need to skip SSR for a component, wrap it in a typeof window !== "undefined" guard.

Things that don't exist in hadars

Some Next.js features simply don't exist in hadars. A few have direct alternatives; others you just don't need.

Next.js featurehadars equivalent / alternative
Static Site Generation (SSG / getStaticProps)hadars export static — pre-renders pages to HTML files. Add source plugins to pull data from any CMS.
Incremental Static Regeneration (ISR)Use the SSR cache option with a TTL, or an external CDN cache. Full ISR is not supported.
Server Components / React RSCNot yet. Use useServerData for component-level server data.
Middleware (edge, request rewriting)Use the fetch hook in config or a reverse proxy (nginx, Cloudflare).
next/image (automatic optimisation)Use your CDN or <img> directly. No build-time image processing.
next/font (automatic font loading)Load fonts in your HTML template or as a <link> in HadarsHead.
Built-in internationalisation (i18n routing)Handle locale in your router and getInitProps, or use i18next.
Parallel / intercepted routesNot applicable — routing is entirely yours.

When to choose hadars

SSR without the framework overhead

No file conventions, no special exports, no compiler transforms beyond what rspack does normally.

You already picked a router

react-router, TanStack Router, wouter — plug it in directly. hadars just passes the URL as a prop.

AWS Lambda

hadars export lambda builds a single self-contained .mjs. No filesystem reads on cold start.

Bun or Deno

Uses the standard Fetch API throughout. Same code runs on Node.js, Bun, and Deno unchanged.

Minimal dependencies

No peer deps beyond React. The only coupling is the config file — swap it out when you outgrow it.

Static sites with a CMS

hadars export static + Gatsby-compatible source plugins. Pull data from Contentful, local files, or any API — no Gatsby required.

Dynamic, authenticated apps

If every response depends on the user's session, SSG buys you nothing. SSR with a short cache TTL is faster to reason about.