Yes, you read that right — I designed and implemented a modern, serverless JavaScript full-stack framework in one day.
This isn’t clickbait. I didn’t do it to flex or impress anyone — I simply needed it for a project I’m working on.
That project is an innovative, AI-first retrieval database, designed to run serverlessly and take advantage of Cloudflare’s edge network.
Why Cloudflare?
Because it’s free to start, cheaper at scale, and gives you global performance out of the box — perfect for modern edge-native apps.
The Stack I Chose
Here’s what powers the framework:
- Backend: Hono – lightweight, fast, and extendable.
- Frontend: React – my go-to for building UI/UX at speed.
- Build System: Vite – blazing fast for both backend and frontend.
- Runtime: Node.js locally, Cloudflare Workers in production.
- Package Ecosystem: NPM – for maximum compatibility with JS libraries.
- Deployment: Cloudflare Workers (backend) + Cloudflare Pages (frontend).
This stack stands shoulder-to-shoulder with popular modern stacks like Vercel’s Next.js or OpenNext — but with fewer limitations and zero vendor lock-in.
You get:
✅ Edge functions powered by Cloudflare Workers
✅ Highly performant, scalable codebase
✅ Great DX (developer experience)
And most importantly — you’re free to build and scale without being boxed in.
Step-by-Step: Building the Framework
We need eight things to make this work:
- Backend: Hono
- Frontend: React
- Bundler: Vite
- Backend–Frontend Link: Vite
- Runtime: Node locally, Workers in production
- Ecosystem: NPM
- Deployment: Cloudflare Workers + Pages
- Developer: (That’s me — the tech wizard Ahmed. 😄)
Folder Structure
We’ll follow a monorepo structure, with:
-
packages/
→ containsclient/
andserver/
-
types/
→ shared types outside ofpackages/
- root configuration files (
package.json
,tsconfig.json
, etc.)
Root Configuration
Here we define configurations that link backend, frontend, and shared packages.
package.json
{
"name": "my-hono-react-app",
"private": true,
"type": "module",
"workspaces": [
"packages/*",
"types"
],
"scripts": {
"dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
"dev:client": "npm run dev -w client",
"dev:server": "npm run dev -w server",
"watch": "concurrently \"npm run watch -w client\" \"npm run watch -w server\"",
"build": "npm run build -w client && npm run build -w server",
"deploy": "npm run deploy -w server"
},
"devDependencies": {
"@rollup/rollup-win32-x64-msvc": "^4.50.1",
"concurrently": "^8.2.2",
"wrangler": "^3.0.0"
},
"dependencies": {
"lightningcss": "^1.30.1"
}
}
💡 Note: If you’re on Windows, you’ll need @rollup/rollup-win32-x64-msvc.
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
},
"include": ["src/**/*", "*.ts"],
}
wrangler.toml
name = "hono-react-app"
main = "src/server/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
NODE_ENV = "development"
[env.production]
[env.production.vars]
NODE_ENV = "production"
# For serving static assets in production
[assets]
directory = "./dist/client"
binding = "ASSETS"
[build]
command = "npm run build"
# For development
[dev]
port = 8787
(And yes, you still need your .gitignore
and README.md
— jk, let’s keep going 😄)
Shared Types
The types
folder stores code shared across client and server. You can create many of these as you wish.
package.json
{
"name": "types",
"private": true,
"type": "module",
"types": "./index.d.ts",
"files": [
"**/*.d.ts",
"**/*.ts"
],
"scripts": {
"build": "tsc --project tsconfig.json"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"declaration": true,
"declarationMap": true,
"outDir": "../dist-types",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules",
"dist-types"
]
}
Packages → Server
package.json
{
"name": "server",
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy --minify",
"build": "npm run build -w client && tsc --noEmit",
"watch": "tsc --noEmit --watch --preserveWatchOutput",
"build:server": "tsc && cp -r ../dist/client ./dist/",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
},
"dependencies": {
"hono": "^4.9.6",
"types": "../types"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.0.0",
"wrangler": "^4.4.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/types": ["../../types/index"]
},
"noEmit": true,
},
"include": ["src/**/*"], // ONLY server files
"exclude": ["node_modules", "dist", "../client"] // Explicitly exclude client
}
wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "server",
"main": "src/index.ts",
"compatibility_date": "2025-09-11",
"pages_build_output_dir": "./dist" // for cloudflare pages main for workers
// "compatibility_flags": [
// "nodejs_compat"
// ],
// "vars": {
// "MY_VAR": "my-variable"
// },
// "kv_namespaces": [
// {
// "binding": "MY_KV_NAMESPACE",
// "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
// }
// ],
// "r2_buckets": [
// {
// "binding": "MY_BUCKET",
// "bucket_name": "my-bucket"
// }
// ],
// "d1_databases": [
// {
// "binding": "MY_DB",
// "database_name": "my-database",
// "database_id": ""
// }
// ],
// "ai": {
// "binding": "AI"
// },
// "observability": {
// "enabled": true,
// "head_sampling_rate": 1
// }
}
src/index.ts
Here we need two things, a way to define our backend APIs which is done by using Hono, the other configurations is to enable Server-side rendering via React and Vite.
import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";
import { cors } from "hono/cors";
const app = new Hono();
// Create a simple manifest object
const staticManifest = {};
// Cross origin
app.use(
"/api/*",
cors({
origin: ["http://localhost:3000", "http://localhost:5173"], // Allow both client ports
credentials: true,
})
);
// Serve static assets from the client build
app.use(
"/assets/*",
serveStatic({
root: "./dist/client",
manifest: staticManifest,
})
);
app.use(
"/favicon.ico",
serveStatic({
path: "./dist/client/favicon.ico",
manifest: staticManifest,
})
);
app.get("/api/hello", (c) => {
return c.json({
message: "Hello from Hono API!",
timestamp: new Date().toISOString(),
});
});
app.get("*", async (c) => {
try {
// Dynamically import the built server entry
// @ts-ignore - This file is generated during build
const { render } = await import("../dist/server/entry-server.js");
const url = new URL(c.req.url);
const { html, state } = await render(url.pathname, {});
const template = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hono + React + Vite</title>
</head>
<body>
<div id="root">${html}</div>
<script>window.__INITIAL_STATE__ = ${state}</script>
<script type="module" src="/assets/client.js"></script>
</body>
</html>`;
return c.html(template);
} catch (e) {
console.error("SSR Error:", e);
return c.html("Server Error", 500);
}
});
export default app;
Packages → Client
We create the client using:
cd packages
npm create vite@latest
Then update tsconfig.app.json
to add type paths.
pages/HomePage.tsx
This isn’t a Next.js pages folder — just a way to organize our code.
We’re using
react-router
for routing to get a SPA-like feel with SSR capability.
import { useQuery } from '@tanstack/react-query'
import { apiFetch } from '../api';
function HomePage() {
const { data: hello, isLoading } = useQuery({
queryKey: ['hello'],
queryFn: async () => {
const response = await apiFetch('/api/hello')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
});
return (
<div className="home-container">
<div className="text-center">
<h1 className="main-title">
Welcome to the Modern Stack
</h1>
<p className="main-description">
Experience NextJS-like DX with Hono, Cloudflare Workers, Vite, and React.
Built for speed, deployed to the edge.
</p>
</div>
<div className="features-grid">
<FeatureCard
title="⚡ Lightning Fast"
description="Powered by Cloudflare Workers with global edge deployment"
/>
<FeatureCard
title="🔥 Hot Reload"
description="Instant updates during development with Vite"
/>
<FeatureCard
title="🛡️ Type Safe"
description="Full TypeScript support across client and server"
/>
<FeatureCard
title="🌐 Edge Computing"
description="Run code closer to your users for minimal latency"
/>
<FeatureCard
title="📦 Zero Config"
description="Sensible defaults with easy customization"
/>
<FeatureCard
title="🚀 Serverless"
description="No servers to manage, scales automatically"
/>
</div>
{/* API Status */}
<div className="api-status-card">
<h3 className="api-status-title">
API Status
</h3>
{isLoading ? (
<div className="loading-pulse">
<div className="pulse-line pulse-line-1"></div>
<div className="pulse-line pulse-line-2"></div>
</div>
) : hello ? (
<div className="api-status-content">
<div className="status-indicator">
<div className="status-dot status-connected"></div>
<span className="status-text">Connected to Hono API</span>
</div>
<p className="api-response">
Response: {hello.message}
</p>
<p className="api-timestamp">
Timestamp: {hello.timestamp}
</p>
</div>
) : (
<div className="status-indicator">
<div className="status-dot status-error"></div>
<span className="status-text">API connection failed</span>
</div>
)}
</div>
</div>
)
}
function FeatureCard({ title, description }: { title: string; description: string }) {
return (
<div className="feature-card">
<h3 className="feature-title">{title}</h3>
<p className="feature-description">{description}</p>
</div>
)
}
export default HomePage
client/src/entry-server.tsx
Handles server-side rendering for client routes.
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
export function render(url: string, initialState : any) {
// Create a new QueryClient for each request
const queryClient = new QueryClient()
const html = renderToString(
<React.StrictMode>
<StaticRouter location={url}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StaticRouter>
</React.StrictMode>
)
// Serialize the state for hydration
const state = JSON.stringify(initialState).replace(/</g, '\\\\u003c')
return { html, state }
}
We configure React Query for state management as well React Router For client side routing client/src/index.tsx:
import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
// Create a client for hydration
const queryClient = new QueryClient()
hydrateRoot(
document.getElementById('root') as HTMLElement,
<React.StrictMode>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>
</React.StrictMode>
)
CSS found in the link of repo below ...
Done. Seriously.
That’s it — we’ve just built a full-stack, serverless JavaScript framework ready to power production apps.
To get started:
npm i
npm run build
npm run dev
Node version: v20.17.0
Link of full source code : https://github.com/ARAldhafeeri/hono-react-vite-cloudflare
You now have a modern, serverless stack that is fast, scalable, and developer-friendly — without being tied down by commercial platforms.
Top comments (0)