Build a todo list Jamstack application
In this tutorial, you will build a todo list application using HTML, CSS, and JavaScript. The application data will be stored in Workers KV.

Before starting this project, you should have some experience with HTML, CSS, and JavaScript. You will learn:
- How building with Workers makes allows you to focus on writing code and ship finished products.
- How the addition of Workers KV makes this tutorial a great introduction to building full, data-driven applications.
If you would like to see the finished code for this project, find the project on GitHub ↗ and refer to the live demo ↗ to review what you will be building.
All of the tutorials assume you have already completed the Get started guide, which gets you set up with a Cloudflare Workers account, C3 ↗, and Wrangler.
First, use the create-cloudflare
↗ CLI tool to create a new Cloudflare Workers project named todos
. In this tutorial, you will use the default Hello World
template to create a Workers project.
npm create cloudflare@latest -- todos
yarn create cloudflare todos
pnpm create cloudflare@latest todos
For setup, select the following options:
- For What would you like to start with?, choose
Hello World example
. - For Which template would you like to use?, choose
Worker only
. - For Which language do you want to use?, choose
JavaScript
. - For Do you want to use git for version control?, choose
Yes
. - For Do you want to deploy your application?, choose
No
(we will be making some changes before deploying).
Move into your newly created directory:
cd todos
Inside of your new todos
Worker project directory, index.js
represents the entry point to your Cloudflare Workers application.
All incoming HTTP requests to a Worker are passed to the fetch()
handler as a request object. After a request is received by the Worker, the response your application constructs will be returned to the user. This tutorial will guide you through understanding how the request/response pattern works and how you can use it to build fully featured applications.
export default { async fetch(request, env, ctx) { return new Response("Hello World!"); },};
In your default index.js
file, you can see that request/response pattern in action. The fetch
constructs a new Response
with the body text 'Hello World!'
.
When a Worker receives a request
, the Worker returns the newly constructed response to the client. Your Worker will serve new responses directly from Cloudflare's global network ↗ instead of continuing to your origin server. A standard server would accept requests and return responses. Cloudflare Workers allows you to respond by constructing responses directly on the Cloudflare global network.
Any project you deploy to Cloudflare Workers can make use of modern JavaScript tooling like ES modules, npm
packages, and async
/await
↗ functions to build your application. In addition to writing Workers, you can use Workers to build full applications using the same tooling and process as in this tutorial.
In this tutorial, you will build a todo list application running on Workers that allows reading data from a KV store and using the data to populate an HTML response to send to the client.
The work needed to create this application is split into three tasks:
- Write data to KV.
- Rendering data from KV.
- Adding todos from the application UI.
For the remainder of this tutorial you will complete each task, iterating on your application, and then publish it to your own domain.
To begin, you need to understand how to populate your todo list with actual data. To do this, use Cloudflare Workers KV — a key-value store that you can access inside of your Worker to read and write data.
To get started with KV, set up a namespace. All of your cached data will be stored inside that namespace and, with configuration, you can access that namespace inside the Worker with a predefined variable. Use Wrangler to create a new namespace called TODOS
with the kv namespace create
command and get the associated namespace ID by running the following command in your terminal:
npx wrangler kv namespace create "TODOS" --preview
The associated namespace can be combined with a --preview
flag to interact with a preview namespace instead of a production namespace. Namespaces can be added to your application by defining them inside your Wrangler configuration. Copy your newly created namespace ID, and in your Wrangler configuration file, define a kv_namespaces
key to set up your namespace:
{ "kv_namespaces": [ { "binding": "TODOS", "id": "<YOUR_ID>", "preview_id": "<YOUR_PREVIEW_ID>" } ]}
kv_namespaces = [ {binding = "TODOS", id = "<YOUR_ID>", preview_id = "<YOUR_PREVIEW_ID>"}]
The defined namespace, TODOS
, will now be available inside of your codebase. With that, it is time to understand the KV API. A KV namespace has three primary methods you can use to interface with your cache: get
, put
, and delete
.
Start storing data by defining an initial set of data, which you will put inside of the cache using the put
method. The following example defines a defaultData
object instead of an array of todo items. You may want to store metadata and other information inside of this cache object later on. Given that data object, use JSON.stringify
to add a string into the cache:
export default { async fetch(request, env, ctx) { const defaultData = { todos: [ { id: 1, name: "Finish the Cloudflare Workers blog post", completed: false, }, ], }; await env.TODOS.put("data", JSON.stringify(defaultData)); return new Response("Hello World!"); },};
Workers KV is an eventually consistent, global datastore. Any writes within a region are immediately reflected within that same region but will not be immediately available in other regions. However, those writes will eventually be available everywhere and, at that point, Workers KV guarantees that data within each region will be consistent.
Given the presence of data in the cache and the assumption that your cache is eventually consistent, this code needs a slight adjustment: the application should check the cache and use its value, if the key exists. If it does not, you will use defaultData
as the data source for now (it should be set in the future) and write it to the cache for future use. After breaking out the code into a few functions for simplicity, the result looks like this:
export default { async fetch(request, env, ctx) { const defaultData = { todos: [ { id: 1, name: "Finish the Cloudflare Workers blog post", completed: false, }, ], }; const setCache = (data) => env.TODOS.put("data", data); const getCache = () => env.TODOS.get("data");
let data;
const cache = await getCache(); if (!cache) { await setCache(JSON.stringify(defaultData)); data = defaultData; } else { data = JSON.parse(cache); }
return new Response(JSON.stringify(data)); },};
Given the presence of data in your code, which is the cached data object for your application, you should take this data and render it in a user interface.
To do this, make a new html
variable in your Workers script and use it to build up a static HTML template that you can serve to the client. In fetch
, construct a new Response
with a Content-Type: text/html
header and serve it to the client:
const html = `<!DOCTYPE html><html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Todos</title> </head> <body> <h1>Todos</h1> </body></html>`;
async fetch (request, env, ctx) { // previous code return new Response(html, { headers: { 'Content-Type': 'text/html' } });}