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.
getServerSideProps / Server ComponentsgetInitProps + useServerDatanext/head / metadata API<HadarsHead> anywhere in the treeProject 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
my-app/
├── app/ # or pages/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/page.tsx
│ └── blog/[slug]/page.tsx
├── public/
├── next.config.js
└── package.jsonmy-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.jsonpages/ 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
// 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>;
}// 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>;
};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)
// 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>;
}// 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)
// 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>;
}// 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
// 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>
</>
);
}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>
</>
);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
// 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 };
}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
// app/api/hello/route.ts
export async function GET(req: Request) {
return Response.json({ message: 'hello' });
}// 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
},
};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
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
);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')
);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.
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.