KV store
Tags: react, cloud
Published: 2022-08-10 14:58:00

In the previous article, we set up our project and deployed a "Hello World" app to Cloudflare Workers. Now we're gonna look at storing and retrieving our data in [Cloudflare Workers KV store](https://developers.cloudflare.com/workers/runtime-apis/kv/). It's a simple but useful key-value store and it has a generous free tier that we can use for our project. We'll start by installing a few dependencies:

```bash
npm install -D @cloudflare/workers-types @miniflare/kv @miniflare/storage-memory
```

`@cloudflare/workers-types` provides global type definitions for the KV API. We'll add it to our `tsconfig.json` file in `compilerOptions.types`:

```json
{
	"compilerOptions": {
		// ... existing compiler options ...
-		"types": ["vite/client"]
+		"types": ["vite/client", "@cloudflare/workers-types"]
	}
}
```

The KV API is only available on Cloudflare Workers. But, during development, Rakkas runs our app on Node.js. Fortunately, the [Miniflare](https://miniflare.dev) project has a KV implementation for Node. The other two packages that we've installed (`@miniflare/kv` and `@miniflare/storage-memory`) are what we need to be able to use the KV API during development. Let's create a `src/kv-mock.ts` file and create a local KV store to store our ublog posts ("twits") while testing:

```ts
import { KVNamespace } from "@miniflare/kv";
import { MemoryStorage } from "@miniflare/storage-memory";

export const postStore = new KVNamespace(new MemoryStorage());

const MOCK_POSTS = [
	{
		key: "1",
		content: "Hello, world!",
		author: "Jane Doe",
		postedAt: "2022-08-10T14:34:00.000Z",
	},
	{
		key: "2",
		content: "Hello ublog!",
		author: "Cody Reimer",
		postedAt: "2022-08-10T13:27:00.000Z",
	},
	{
		key: "3",
		content: "Wow, this is pretty cool!",
		author: "Zoey Washington",
		postedAt: "2022-08-10T12:00:00.000Z",
	},
];

// We'll add some mock posts
// Rakkas supports top level await
await Promise.all(
	// We'll do this in parallel with Promise.all,
	// just to be cool.
	MOCK_POSTS.map((post) =>
		postStore.put(post.key, post.content, {
			metadata: {
				author: post.author,
				postedAt: post.postedAt,
			},
		})
	)
);
```

As you can see we also added some mock data because our application doesn't have a "create post" feature yet. This way, we can start fetching and showing some posts before we implement it.

The `put` method of the store accepts a key, a value, and some optional metadata. We'll use the content to store the actual post content, and the metadata to store the author and the date the post was created. The key has to be unique but other than that it is currently meaningless, we'll get to that later.

Now we should make this store available to our application's server-side code. The best place to do it is the [HatTip entry](https://rakkasjs.org/guide/hattip-entry) which is the main server-side entry point of a Rakkas application. It's an optional file which is not part of the generated boilerplate so we'll add it manually as `src/entry-hattip.ts`:

```ts
import { createRequestHandler } from "rakkasjs";

declare module "rakkasjs" {
	interface ServerSideLocals {
		postStore: KVNamespace;
	}
}

export default createRequestHandler({
	middleware: {
		beforePages: [
			async (ctx) => {
				if (import.meta.env.DEV) {
					const { postStore } = await import("./kv-mock");
					ctx.locals.postStore = postStore;
				} else {
					ctx.locals.postStore = (ctx.platform as any).env.KV_POSTS;
				}
			},
		],
	},
});
```

Woah, that's a lot of unfamiliar stuff. Let's break it down.

The HatTip entry is supposed to default export a HatTip request handler. So we create one with `createRequestHandler`. `createRequestHandler` accepts a bunch of options to customize the server's behavior. One of them is `middleware` which is used to inject middleware functions into Rakkas's request handling pipeline. [HatTip middlewares](https://github.com/hattipjs/hattip#middleware-system) are similar to Express middlewares in many ways. So the concept should be familiar if you've used Express before.

We add our middleware before Rakkas processes our application's pages (`beforePages`). It's, in fact, the earliest interception point. In the middleware, we inject our store into the request context object which will be available to our application's server-side code. The request context object has a `locals` property dedicated to storing application-specific stuff like this.

The bit starting with `declare module "rakkasjs"` is [a TypeScript technique](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) for extending interfaces declared in other modules. In this case, we're extending the `ServerSideLocals` interface which is the type of `ctx.locals` where `ctx` is the request context object.

`import.meta.env.DEV` is a [Vite feature](https://vitejs.dev/guide/env-and-mode.html). Its value is `true` during development and `false` in production. Here, we use it to determine whether we should create a mock KV store or use the real one on Cloudflare Workers.

For production, HatTip's Cloudflare Workers adapter makes the so-called [bindings](https://developers.cloudflare.com/workers/platform/bindings/) available in [`ctx.platform.env`](https://github.com/hattipjs/hattip/tree/main/packages/adapter/adapter-cloudflare-workers#contextplatform). `ctx.platform`'s type is `unknown` because it changes depending on the environment. So we use `as any` to appease the TypeScript compiler. `KV_POSTS` is just a name we've chosen for the binding name of our posts store.

Thanks to this fairly simple middleware, the KV store that will hold our posts will be available to our application as `ctx.locals.postStore` where `ctx` is the request context.

## Fetching data from the KV store

Now we'll spin up a dev server with `npm run dev` and edit the `src/pages/index.page.tsx` file to fetch and display our mock posts. Rakkas has a very cool data fetching hook called [`useServerSideQuery`](https://rakkasjs.org/guide/use-server-side-query). With this hook, you can put your server-side code **right inside your components** without having to create API endpoints:

```tsx
import { useServerSideQuery } from "rakkasjs";

export default function HomePage() {
	const { data: posts } = useServerSideQuery(async (ctx) => {
		// This callback always runs on the server.
		// So we have access to the request context!

		// Get a list of the keys and metadata
		const list = await ctx.locals.postStore.list<{
			author: string;
			postedAt: string;
		}>();

		// Get individual posts and move things around
		// a little to make it easier to render
		const posts = await Promise.all(
			list.keys.map((key) =>
				ctx.locals.postStore
					.get(key.name)
					.then((data) => ({ key, content: data }))
			)
		);

		return posts;
	});

	return (
		<main>
			<h1>Posts</h1>
			<ul>
				{posts.map((post) => (
					<li key={post.key.name}>
						<div>{post.content}</div>
						<div>
							{/* post.key.metadata may not be available while testing for */}
							{/* reasons we'll cover later. That's why we need the nullish */}
							{/* checks here */}
							<i>{post.key.metadata?.author ?? "Unknown author"}</i>
							&nbsp;
							<span>
								{post.key.metadata
									? new Date(post.key.metadata.postedAt).toLocaleString()
									: "Unknown date"}
							</span>
						</div>
						<hr />
					</li>
				))}
			</ul>
		</main>
	);
}
```

That's it! Now you should see a list of mock posts if you visit `http://localhost:5173`. Don't worry about the ugly looks yet. We'll cover styling later.

## Building for production

Now build your application for production and deploy it:

```bash
npm run build
npm run deploy
```

Unfortunately, if you visit your production URL now, you will get an error. That's because we haven't created a KV store on Cloudflare Workers yet. We'll do that with the `wrangler` CLI::

```bash
npx wrangler kv:namespace create KV_POSTS
```

If everything goes well, you should see a message like this:

```
Add the following to your configuration file in your kv_namespaces array:
{ binding = "KV_POSTS", id = "<YOUR_KV_NAMESPACE_ID>" }
```

We will do just that and we will add the following at the end of our `wrangler.toml` file:

```toml
[[kv_namespaces]]
binding = "KV_POSTS"
id = "<YOUR_KV_NAMESPACE_ID>"
```

Then we will deploy again with `npm run deploy`. This time the error is gone but we'll still not see any posts. Let's add a few with `wrangler` CLI:

```bash
npx wrangler kv:key put --binding KV_POSTS 1 "Hello world!"
npx wrangler kv:key put --binding KV_POSTS 2 "Ooh! Pretty nice!"
npx wrangler kv:key put --binding KV_POSTS 3 "Wrangler lets us add new values to KV!"
```

Unfortunately, `wrangler` CLI doesn't allow us to add metadata to our posts so we'll see "Unknown author" and "Unknown date" in the UI but other than that... **IT WORKS, YAY!** We have a working data store for our app!

You can also visit the [Cloudflare Dashboard](https://dash.cloudflare.com) and go to Workers > KV to add/remove/edit your values in your store. If you do, you will notice that Cloudflare uses the same KV store mechanism to store your static assets.

## Cleaning up

If you're going to put your code in a public repo, you should not expose your KV store ID. Just make a copy of your `wrangler.toml` as `wrangler.example.toml` and redact the KV store ID from the copy. Then add `wrangler.toml` to your `.gitignore` and run `git rm wrangler.toml --cached` before committing. I'm not entirely sure whether this is required but there has been a data breach in the past involving the KV store ID so it's best to play safe.

## What's next?

In the next article, we'll add a form to allow users to add new posts.

> You can find the progress up to this point on [GitHub](https://github.com/rakkasjs/ublog/tree/article-2).