<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

Example project demonstrating raw HTTP fetch handling with Hono integration.

## Getting Started

```sh
git clone https://github.com/rivet-dev/rivet.git
cd rivet/examples/raw-fetch-handler
npm install
npm run dev
```


## Features

- **Raw fetch handlers**: Use `onRequest` for low-level HTTP request handling with custom routing
- **Hono integration**: Embed Hono router inside actor fetch handlers using `createVars`
- **HTTP endpoints**: Define custom HTTP endpoints directly within actors
- **Proxy routing**: Forward HTTP requests from external endpoints to actor fetch handlers
- **Multiple actor instances**: Each named counter maintains independent state

## Implementation

The backend defines a counter actor with a Hono router embedded in the `onRequest` handler. Each counter is identified by a unique name, and the frontend can interact with counters through direct actor fetch calls or HTTP requests through a forward endpoint. Multiple counters maintain independent state.

### Key Implementation

- **Actor Definition** ([`src/backend/registry.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/raw-fetch-handler/src/backend/registry.ts)): Demonstrates `onRequest` handler with Hono router for custom HTTP routing

## Project Structure

```
raw-fetch-handler/
├── src/
│   ├── backend/     # RivetKit server with counter actors
│   └── frontend/    # React app demonstrating client interactions
└── tests/           # Vitest test suite
```

## Resources

Read more about [HTTP request handling](/docs/actors/http), [state](/docs/actors/state), and [actions](/docs/actors/actions).

## License

MIT
">
<input type="hidden" name="project[files][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;/frontend/main.tsx&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;">
<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;vite&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest run&quot;,&quot;build&quot;:&quot;vite build &amp;&amp; vite build --mode server&quot;,&quot;start&quot;:&quot;srvx --static=public/ dist/server.js&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;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;,&quot;vite-plugin-srvx&quot;:&quot;^1.0.0&quot;},&quot;dependencies&quot;:{&quot;@hono/node-server&quot;:&quot;^1.19.1&quot;,&quot;@hono/node-ws&quot;:&quot;^1.3.0&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@b7a6ac3488d8d64f2cddf185312f05cf519f9413&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;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@b7a6ac3488d8d64f2cddf185312f05cf519f9413&quot;,&quot;srvx&quot;:&quot;^0.10.0&quot;},&quot;template&quot;:{&quot;technologies&quot;:[&quot;typescript&quot;],&quot;tags&quot;:[],&quot;priority&quot;:1000,&quot;frontendPort&quot;:5173},&quot;license&quot;:&quot;MIT&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;allowImportingTsExtensions&quot;: true,
		&quot;rewriteRelativeImportExtensions&quot;: true
	},
	&quot;include&quot;: [&quot;src/**/*&quot;, &quot;tests/**/*&quot;, &quot;frontend/**/*&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;],
	&quot;tasks&quot;: {
		&quot;build&quot;: {
			&quot;dependsOn&quot;: [&quot;@rivetkit/react#build&quot;, &quot;rivetkit#build&quot;]
		}
	}
}
">
<input type="hidden" name="project[files][vercel.json]" value="{
	&quot;framework&quot;: &quot;hono&quot;
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import srvx from &quot;vite-plugin-srvx&quot;;

export default defineConfig({
	plugins: [react(), ...srvx({ entry: &quot;src/server.ts&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][frontend/App.tsx]" value="import { useState, useEffect } from &quot;react&quot;;
import { createClient } from &quot;@rivetkit/react&quot;;
import type { registry } from &quot;../src/actors.ts&quot;;

// Create a client that connects to the running server
const client = createClient&lt;typeof registry&gt;(`${window.location.origin}/api/rivet`);

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
			// FIXME: Use metadata&#39;s clientEndpoint
			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][frontend/main.tsx]" value="import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import App from &quot;./App.tsx&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[files][src/actors.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() };
	},
	onRequest: (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/server.ts]" value="import { Hono } from &quot;hono&quot;;
import { cors } from &quot;hono/cors&quot;;
import { createClient } from &quot;rivetkit/client&quot;;
import { registry } from &quot;./actors.ts&quot;;

const client = createClient&lt;typeof registry&gt;();

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;
});

app.all(&quot;/api/rivet/*&quot;, (c) =&gt; registry.handler(c.req.raw));

export default app;

export { client };
">
<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/actors.ts&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[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>