<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][README.md]" value="# Raw Fetch Handler Example for RivetKit

Example project demonstrating raw HTTP fetch handling with Hono integration in [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Overview

This example demonstrates:
- Using Hono router inside an actor&#39;s `onFetch` handler via `createVars`
- Creating named counter actors that maintain independent state
- Making fetch requests to actors through the frontend client
- Forwarding requests from custom Hono endpoints to actor fetch handlers
- Building a React frontend that interacts with RivetKit actors
- Testing actors with fetch handlers

## Project Structure

```
raw-fetch-handler/
├── src/
│   ├── backend/     # RivetKit server with counter actors
│   └── frontend/    # React app demonstrating client interactions
└── tests/           # Vitest test suite
```

## Getting Started

### Prerequisites

- Node.js

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/raw-fetch-handler
pnpm install
```

### Development

Start both backend and frontend:

```sh
pnpm dev
```

Or run them separately:

```sh
# Terminal 1 - Backend
pnpm dev:backend

# Terminal 2 - Frontend
pnpm dev:frontend
```

Run tests:

```sh
pnpm test
```

## Features

### Backend

1. **Counter Actor** - A simple counter with HTTP endpoints
   - `GET /count` - Get current count
   - `POST /increment` - Increment the counter

2. **Forward Endpoint** - Routes requests to actor fetch handlers
   - `/forward/:name/*` - Forward any request to the named actor

### Frontend

A React app demonstrating:
- Creating multiple named counters
- Interacting via actor fetch API
- Using the forward endpoint
- Real-time state updates

## How It Works

1. The backend defines a counter actor with a Hono router
2. Each counter is identified by a unique name
3. The frontend can interact with counters in two ways:
   - Direct actor fetch calls using the RivetKit client
   - HTTP requests through the forward endpoint
4. Multiple counters maintain independent state

## License

Apache 2.0">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-raw-fetch-handler&quot;,&quot;version&quot;:&quot;2.0.21&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;concurrently \&quot;tsx --watch src/backend/server.ts\&quot; \&quot;vite\&quot;&quot;,&quot;dev:backend&quot;:&quot;tsx --watch src/backend/server.ts&quot;,&quot;dev:frontend&quot;:&quot;vite&quot;,&quot;build&quot;:&quot;vite build&quot;,&quot;preview&quot;:&quot;vite preview&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest&quot;},&quot;dependencies&quot;:{&quot;@hono/node-server&quot;:&quot;^1.14.0&quot;,&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@09a68bcf14620b1ced7fb1e6f33d3d0503298a6f&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@09a68bcf14620b1ced7fb1e6f33d3d0503298a6f&quot;,&quot;hono&quot;:&quot;^4.6.18&quot;,&quot;react&quot;:&quot;^18.3.1&quot;,&quot;react-dom&quot;:&quot;^18.3.1&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^22.10.6&quot;,&quot;@types/react&quot;:&quot;^18.3.18&quot;,&quot;@types/react-dom&quot;:&quot;^18.3.5&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.3.4&quot;,&quot;concurrently&quot;:&quot;^9.1.2&quot;,&quot;tsx&quot;:&quot;^4.20.0&quot;,&quot;typescript&quot;:&quot;^5.7.3&quot;,&quot;vite&quot;:&quot;^5.4.19&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;}}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
	&quot;compilerOptions&quot;: {
		&quot;target&quot;: &quot;ES2022&quot;,
		&quot;module&quot;: &quot;ESNext&quot;,
		&quot;lib&quot;: [&quot;ES2022&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
		&quot;jsx&quot;: &quot;react-jsx&quot;,
		&quot;moduleResolution&quot;: &quot;bundler&quot;,
		&quot;strict&quot;: true,
		&quot;esModuleInterop&quot;: true,
		&quot;skipLibCheck&quot;: true,
		&quot;forceConsistentCasingInFileNames&quot;: true,
		&quot;resolveJsonModule&quot;: true,
		&quot;allowSyntheticDefaultImports&quot;: true,
		&quot;noEmit&quot;: true,
		&quot;types&quot;: [&quot;vitest/globals&quot;, &quot;node&quot;]
	},
	&quot;include&quot;: [&quot;src/**/*&quot;, &quot;tests/**/*&quot;, &quot;vite.config.ts&quot;],
	&quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
	&quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
	&quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import react from &quot;@vitejs/plugin-react&quot;;
import { defineConfig } from &quot;vite&quot;;

export default defineConfig({
	plugins: [react()],
	root: &quot;src/frontend&quot;,
	build: {
		outDir: &quot;../../dist&quot;,
	},
	server: {
		host: &quot;0.0.0.0&quot;,
	},
});
">
<input type="hidden" name="project[files][vitest.config.ts]" value="import { defineConfig } from &quot;vitest/config&quot;;

export default defineConfig({
	test: {
		include: [&quot;tests/**/*.test.ts&quot;],
	},
});
">
<input type="hidden" name="project[files][tests/counter.test.ts]" value="import { setupTest } from &quot;rivetkit/test&quot;;
import { describe, expect, test } from &quot;vitest&quot;;
import { registry } from &quot;../src/backend/registry&quot;;

describe(&quot;Counter Actor&quot;, () =&gt; {
	test(&quot;fetch handler returns counter state&quot;, async (test) =&gt; {
		const { client } = await setupTest(test, registry);
		const handle = client.counter.getOrCreate(&quot;test-fetch&quot;);

		// GET current state
		let response = await handle.fetch(&quot;/count&quot;);
		expect(response.status).toBe(200);
		let data = await response.json();
		expect(data.count).toBe(0);

		// POST to increment
		response = await handle.fetch(&quot;/increment&quot;, { method: &quot;POST&quot; });
		expect(response.status).toBe(200);
		data = await response.json();
		expect(data.count).toBe(1);

		// Verify state persisted with another GET
		response = await handle.fetch(&quot;/count&quot;);
		data = await response.json();
		expect(data.count).toBe(1);
	});
});
">
<input type="hidden" name="project[files][src/backend/registry.ts]" value="import { Hono } from &quot;hono&quot;;
import { type ActorContextOf, actor, setup } from &quot;rivetkit&quot;;

export const counter = actor({
	state: {
		count: 0,
	},
	createVars: () =&gt; {
		// Setup router
		return { router: createCounterRouter() };
	},
	onFetch: (c, request) =&gt; {
		return c.vars.router.fetch(request, { actor: c });
	},
	actions: {
		// ...actions...
	},
});

function createCounterRouter(): Hono&lt;any&gt; {
	const app = new Hono&lt;{
		Bindings: { actor: ActorContextOf&lt;typeof counter&gt; };
	}&gt;();

	app.get(&quot;/count&quot;, (c) =&gt; {
		const { actor } = c.env;

		return c.json({
			count: actor.state.count,
		});
	});

	app.post(&quot;/increment&quot;, (c) =&gt; {
		const { actor } = c.env;

		actor.state.count++;
		return c.json({
			count: actor.state.count,
		});
	});

	return app;
}

export const registry = setup({
	use: { counter },
});
">
<input type="hidden" name="project[files][src/backend/server.ts]" value="import { serve } from &quot;@hono/node-server&quot;;
import { Hono } from &quot;hono&quot;;
import { cors } from &quot;hono/cors&quot;;
import { registry } from &quot;./registry&quot;;

// Start RivetKit
const { client } = registry.start();

// Setup router
const app = new Hono();

app.use(
	cors({
		origin: &quot;http://localhost:5173&quot;,
		credentials: true,
	}),
);

app.get(&quot;/&quot;, (c) =&gt; {
	return c.json({ message: &quot;Fetch Handler Example Server&quot; });
});

// Forward requests to actor&#39;s fetch handler
app.all(&quot;/forward/:name/*&quot;, async (c) =&gt; {
	const name = c.req.param(&quot;name&quot;);

	// Create new URL with the path truncated
	const truncatedPath = c.req.path.replace(`/forward/${name}`, &quot;&quot;);
	const url = new URL(truncatedPath, c.req.url);
	const newRequest = new Request(url, c.req.raw);

	// Forward to actor&#39;s fetch handler
	const actor = client.counter.getOrCreate(name);
	const response = await actor.fetch(truncatedPath, newRequest);

	return response;
});

serve({ fetch: app.fetch, port: 8080 });
console.log(&quot;Listening on port 8080&quot;);

export { client };
">
<input type="hidden" name="project[files][src/frontend/App.tsx]" value="import { useState, useEffect } from &quot;react&quot;;
import { createClient } from &quot;@rivetkit/react&quot;;
import type { registry } from &quot;../backend/registry&quot;;

// Create a client that connects to the running server
const client = createClient&lt;typeof registry&gt;(&quot;http://localhost:8080&quot;);

function Counter({ name }: { name: string }) {
	const [count, setCount] = useState&lt;number | null&gt;(null);
	const [loading, setLoading] = useState(false);

	const actor = client.counter.getOrCreate([name]);

	const fetchCount = async () =&gt; {
		const response = await actor.fetch(&quot;/count&quot;);
		const data = await response.json();
		setCount(data.count);
	};

	const handleIncrement = async () =&gt; {
		setLoading(true);
		try {
			// Method 1: Using fetch API
			const response = await actor.fetch(&quot;/increment&quot;, { method: &quot;POST&quot; });
			const data = await response.json();
			setCount(data.count);
		} finally {
			setLoading(false);
		}
	};

	const handleForwardIncrement = async () =&gt; {
		setLoading(true);
		try {
			// Method 2: Using the forward endpoint
			const response = await fetch(`http://localhost:8080/forward/${name}/increment`, {
				method: &quot;POST&quot;,
			});
			const data = await response.json();
			setCount(data.count);
		} finally {
			setLoading(false);
		}
	};

	useEffect(() =&gt; {
		fetchCount();
	}, []);

	return (
		&lt;div&gt;
			&lt;h2&gt;{name}&lt;/h2&gt;
			&lt;p&gt;Count: {count !== null ? count : &quot;Loading...&quot;}&lt;/p&gt;
			
			&lt;h3&gt;Via Actor Fetch&lt;/h3&gt;
			&lt;button onClick={handleIncrement} disabled={loading}&gt;
				Increment
			&lt;/button&gt;
			
			&lt;h3&gt;Via Forward Endpoint&lt;/h3&gt;
			&lt;button onClick={handleForwardIncrement} disabled={loading}&gt;
				Increment
			&lt;/button&gt;
			
			&lt;br /&gt;
			&lt;button onClick={fetchCount} disabled={loading}&gt;
				Refresh
			&lt;/button&gt;
			&lt;hr /&gt;
		&lt;/div&gt;
	);
}

function App() {
	const [counters, setCounters] = useState([&quot;counter-1&quot;, &quot;counter-2&quot;]);
	const [newCounterName, setNewCounterName] = useState(&quot;&quot;);

	const addCounter = () =&gt; {
		if (newCounterName &amp;&amp; !counters.includes(newCounterName)) {
			setCounters([...counters, newCounterName]);
			setNewCounterName(&quot;&quot;);
		}
	};

	return (
		&lt;div&gt;
			&lt;h1&gt;RivetKit Raw Fetch Handler Example&lt;/h1&gt;
			
			&lt;div&gt;
				&lt;input
					type=&quot;text&quot;
					value={newCounterName}
					onChange={(e) =&gt; setNewCounterName(e.target.value)}
					placeholder=&quot;Counter name&quot;
					onKeyPress={(e) =&gt; e.key === &quot;Enter&quot; &amp;&amp; addCounter()}
				/&gt;
				&lt;button onClick={addCounter}&gt;Add Counter&lt;/button&gt;
			&lt;/div&gt;
			
			&lt;hr /&gt;
			
			&lt;div&gt;
				{counters.map((name) =&gt; (
					&lt;Counter key={name} name={name} /&gt;
				))}
			&lt;/div&gt;
		&lt;/div&gt;
	);
}

export default App;
">
<input type="hidden" name="project[files][src/frontend/index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;RivetKit Fetch Handler Example&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/main.tsx&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;">
<input type="hidden" name="project[files][src/frontend/main.tsx]" value="import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import App from &quot;./App&quot;;

ReactDOM.createRoot(document.getElementById(&quot;root&quot;)!).render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;
);">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="example-raw-fetch-handler">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>