Build a todo list Jamstack application·续

const html = (todos) => `
<!doctype html>
<html>
  <!-- existing content -->
  <script>
    var populateTodos = function() {
      var todoContainer = document.querySelector("#todos")
      todoContainer.innerHTML = null
      window.todos.forEach(todo => {
        var el = document.createElement("div")
        el.textContent = todo.name
        todoContainer.appendChild(el)
      })
    }

populateTodos()

var createTodo = function() {
      var input = document.querySelector("input[name=name]")
      if (input.value.length) {
        todos = [].concat(todos, {
          id: todos.length + 1,
          name: input.value,
          completed: false,
        })
        fetch("/", {
          method: "PUT",
          body: JSON.stringify({ todos: todos }),
        })
        populateTodos()
        input.value = ""
      }
    }

document.querySelector("#create").addEventListener("click", createTodo)
  </script>
`;

With the client-side code in place, deploying the new version of the function should put all these pieces together. The result is an actual dynamic todo list.

5. Update todos from the application UI

For the final piece of your todo list, you need to be able to update todos — specifically, marking them as completed.

Luckily, a great deal of the infrastructure for this work is already in place. You can update the todo list data in the cache, as evidenced by your createTodo function. Performing updates on a todo is more of a client-side task than a Worker-side one.

To start, the populateTodos function can be updated to generate a div for each todo. In addition, move the name of the todo into a child element of that div:

const html = (todos) => `
<!doctype html>
<html>
  <!-- existing content -->
  <script>
    var populateTodos = function() {
      var todoContainer = document.querySelector("#todos")
      todoContainer.innerHTML = null
      window.todos.forEach(todo => {
        var el = document.createElement("div")
        var name = document.createElement("span")
        name.textContent = todo.name
        el.appendChild(name)
        todoContainer.appendChild(el)
      })
    }
  </script>
`;

You have designed the client-side part of this code to handle an array of todos and render a list of HTML elements. There is a number of things that you have been doing that you have not quite had a use for yet – specifically, the inclusion of IDs and updating the todo's completed state. These things work well together to actually support updating todos in the application UI.

To start, it would be useful to attach the ID of each todo in the HTML. By doing this, you can then refer to the element later in order to correspond it to the todo in the JavaScript part of your code. Data attributes and the corresponding dataset method in JavaScript are a perfect way to implement this. When you generate your div element for each todo, you can attach a data attribute called todo to each div:

const html = (todos) => `
<!doctype html>
<html>
  <!-- existing content -->
  <script>
    var populateTodos = function() {
      var todoContainer = document.querySelector("#todos")
      todoContainer.innerHTML = null
      window.todos.forEach(todo => {
        var el = document.createElement("div")
        el.dataset.todo = todo.id

var name = document.createElement("span")
        name.textContent = todo.name

el.appendChild(name)
        todoContainer.appendChild(el)
      })
    }
  </script>
`;

Inside your HTML, each div for a todo now has an attached data attribute, which looks like:

<div data-todo="1"></div>
<div data-todo="2"></div>

You can now generate a checkbox for each todo element. This checkbox will default to unchecked for new todos but you can mark it as checked as the element is rendered in the window:

const html = (todos) => `
<!doctype html>
<html>
  <!-- existing content -->
  <script>
    window.todos.forEach(todo => {
      var el = document.createElement("div")
      el.dataset.todo = todo.id

var name = document.createElement("span")
      name.textContent = todo.name

var checkbox = document.createElement("input")
      checkbox.type = "checkbox"
      checkbox.checked = todo.completed ? 1 : 0

el.appendChild(checkbox)
      el.appendChild(name)
      todoContainer.appendChild(el)
    })
  </script>
`;

The checkbox is set up to correctly reflect the value of completed on each todo but it does not yet update when you actually check the box. To do this, attach the completeTodo function as an event listener on the click event. Inside the function, inspect the checkbox element, find its parent (the todo div), and use its todo data attribute to find the corresponding todo in the data array. You can toggle the completed status, update its properties, and rerender the UI:

const html = (todos) => `
<!doctype html>
<html>
  <!-- existing content -->
  <script>
    var populateTodos = function() {
      window.todos.forEach(todo => {
        // Existing todo element set up code
        checkbox.addEventListener("click", completeTodo)
      })
    }

var completeTodo = function(evt) {
      var checkbox = evt.target
      var todoElement = checkbox.parentNode

var newTodoSet = [].concat(window.todos)
      var todo = newTodoSet.find(t => t.id == todoElement.dataset.todo)
      todo.completed = !todo.completed
      todos = newTodoSet
      updateTodos()
    }
  </script>
`;

The final result of your code is a system that checks the todos variable, updates your Cloudflare KV cache with that value, and then does a re-render of the UI based on the data it has locally.

6. Conclusion and next steps

By completing this tutorial, you have built a static HTML, CSS, and JavaScript application that is transparently powered by Workers and Workers KV, which take full advantage of Cloudflare's global network.

If you would like to keep improving on your project, you can implement a better design (you can refer to a live version available at todos.signalnerve.workers.dev ↗︎), or make additional improvements to security and speed.

You may also want to add user-specific caching. Right now, the cache key is always data – this means that any visitor to the site will share the same todo list with other visitors. Within your Worker, you could use values from the client request to create and maintain user-specific lists. For example, you may generate a cache key based on the requesting IP:

export default {
    async fetch(request, env, ctx) {
        const defaultData = {
            todos: [
                {
                    id: 1,
                    name: "Finish the Cloudflare Workers blog post",
                    completed: false,
                },
            ],
        };
        const setCache = (key, data) => env.TODOS.put(key, data);
        const getCache = (key) => env.TODOS.get(key);

const ip = request.headers.get("CF-Connecting-IP");
        const myKey = `data-${ip}`;

if (request.method === "PUT") {
            const body = await request.text();
            try {
                JSON.parse(body);
                await setCache(myKey, body);
                return new Response(body, { status: 200 });
            } catch (err) {
                return new Response(err, { status: 500 });
            }
        }

let data;

const cache = await getCache();
        if (!cache) {
            await setCache(myKey, JSON.stringify(defaultData));
            data = defaultData;
        } else {
            data = JSON.parse(cache);
        }

const body = html(JSON.stringify(data.todos).replace(/</g, "\\u003c"));

return new Response(body, {
            headers: {
                "Content-Type": "text/html",
            },
        });
    },
};

After making these changes and deploying the Worker one more time, your todo list application now includes per-user functionality while still taking full advantage of Cloudflare's global network.

The final version of your Worker script should look like this:

const html = (todos) => `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Todos</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet"></link>
  </head>

<body class="bg-blue-100">
    <div class="w-full h-full flex content-center justify-center mt-8">
      <div class="bg-white shadow-md rounded px-8 pt-6 py-8 mb-4">
        <h1 class="block text-grey-800 text-md font-bold mb-2">Todos</h1>
        <div class="flex">
          <input class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-800 leading-tight focus:outline-none focus:shadow-outline" type="text" name="name" placeholder="A new todo"></input>
          <button class="bg-blue-500 hover:bg-blue-800 text-white font-bold ml-2 py-2 px-4 rounded focus:outline-none focus:shadow-outline" id="create" type="submit">Create</button>
        </div>
        <div class="mt-4" id="todos"></div>
      </div>
    </div>
  </body>

<script>
    window.todos = ${todos}

var updateTodos = function() {
      fetch("/", { method: "PUT", body: JSON.stringify({ todos: window.todos }) })
      populateTodos()
    }

var completeTodo = function(evt) {
      var checkbox = evt.target
      var todoElement = checkbox.parentNode
      var newTodoSet = [].concat(window.todos)
      var todo = newTodoSet.find(t => t.id == todoElement.dataset.todo)
      todo.completed = !todo.completed
      window.todos = newTodoSet
      updateTodos()
    }

var populateTodos = function() {
      var todoContainer = document.querySelector("#todos")
      todoContainer.innerHTML = null

window.todos.forEach(todo => {
        var el = document.createElement("div")
        el.className = "border-t py-4"
        el.dataset.todo = todo.id

var name = document.createElement("span")
        name.className = todo.completed ? "line-through" : ""
        name.textContent = todo.name

var checkbox = document.createElement("input")
        checkbox.className = "mx-4"
        checkbox.type = "checkbox"
        checkbox.checked = todo.completed ? 1 : 0
        checkbox.addEventListener("click", completeTodo)

el.appendChild(checkbox)
        el.appendChild(name)
        todoContainer.appendChild(el)
      })
    }

populateTodos()

var createTodo = function() {
      var input = document.querySelector("input[name=name]")
      if (input.value.length) {
        window.todos = [].concat(todos, { id: window.todos.length + 1, name: input.value, completed: false })
        input.value = ""
        updateTodos()
      }
    }

document.querySelector("#create").addEventListener("click", createTodo)
  </script>
</html>
`;

export default {
    async fetch(request, env, ctx) {
        const defaultData = {
            todos: [
                {
                    id: 1,
                    name: "Finish the Cloudflare Workers blog post",
                    completed: false,
                },
            ],
        };
        const setCache = (key, data) => env.TODOS.put(key, data);
        const getCache = (key) => env.TODOS.get(key);

const ip = request.headers.get("CF-Connecting-IP");
        const myKey = `data-${ip}`;

if (request.method === "PUT") {
            const body = await request.text();
            try {
                JSON.parse(body);
                await setCache(myKey, body);
                return new Response(body, { status: 200 });
            } catch (err) {
                return new Response(err, { status: 500 });
            }
        }

let data;

const cache = await getCache();
        if (!cache) {
            await setCache(myKey, JSON.stringify(defaultData));
            data = defaultData;
        } else {
            data = JSON.parse(cache);
        }

const body = html(JSON.stringify(data.todos).replace(/</g, "\\u003c"));

return new Response(body, {
            headers: {
                "Content-Type": "text/html",
            },
        });
    },
};

You can find the source code for this project, as well as a README with deployment instructions, on GitHub ↗︎.

Was this helpful?

YesNo

On this page

Build a todo list Jamstack application·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/#page","headline":"Build a todo list Jamstack application","description":"This tutorial explains how to build a todo list application using HTML, CSS, and JavaScript.","url":"https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/og.png?v=139d8563cef1628c","dateModified":"2026-08-25","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript"]}


description: This tutorial shows you how to build and publish a Worker application that generates QR codes. The final version of the codebase is available on GitHub. title: Build a QR code generator image: https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator/og.png?v=2a13fae053e97eb0


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt
Use this file to discover all available pages before exploring further.