DEV Community

Cover image for I Built a Modern Serverless JS Full-Stack Framework in One Day
Ahmed Rakan
Ahmed Rakan

Posted on

I Built a Modern Serverless JS Full-Stack Framework in One Day

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:

  1. Backend: Hono
  2. Frontend: React
  3. Bundler: Vite
  4. Backend–Frontend Link: Vite
  5. Runtime: Node locally, Workers in production
  6. Ecosystem: NPM
  7. Deployment: Cloudflare Workers + Pages
  8. Developer: (That’s me — the tech wizard Ahmed. 😄)

Folder Structure

We’ll follow a monorepo structure, with:

  • packages/ → contains client/ and server/
  • types/ → shared types outside of packages/
  • 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"
  }
}

Enter fullscreen mode Exit fullscreen mode

💡 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"],
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

(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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  // }
}
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

Packages → Client

We create the client using:

cd packages 
npm create vite@latest

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
}

Enter fullscreen mode Exit fullscreen mode

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>
)

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)