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. 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:
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
:
{
"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 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:
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 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
:
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 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 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. 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 available in ctx.platform.env
. 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
. With this hook, you can put your server-side code right inside your components without having to create API endpoints:
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>
<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:
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::
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:
[[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:
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 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.
Top comments (0)