Introduction
This post explores some of the basics of using Cloudflare Durable Objects (Durable Objects and Using Durable Objects).
Up until now, I haven’t had a need to use Durable Objects. Most of my use cases revolve around simple APIs and Workers KV has been sufficient for all of my needs. I’ve been meaning to experiment with Durable Objects and this is my first attempt.
While writing this example, I stumbled across itty-durable. It significantly reduces the amount of code required and seems worthy of a later look.
The Cloudflare official durable-objects-template. The code for this example will be linked in Step 5.
What is a Durable Object?
Durable Objects are named instances of a class you define. Like a class in object-oriented programming, the class defines the methods and data a Durable Object can access.
Durable Objects provide low-latency coordination and consistent storage for the Workers platform. A given namespace can support essentially unlimited Durable Objects, with each Object having access to a transactionally consistent key-value storage API.
Durable Objects consist of two components: a class that defines a template for creating Durable Objects and a Workers script that instantiates and uses those Durable Objects. The class and the Workers script are linked together with a binding.
A Durable Object remains active until all asynchronous I/O, including Promises, within the Durable Object has resolved. This is true for all HTTP and/or WebSocket connections. Durable Object state can be read and written to persistent storage using the Transactional storage API.
Durable Objects can be accessed via a Worker using alarm() (not covered in this post) and fetch() handler methods. The fetch() method takes a Request as the parameter and returns a Response (or a Promise for a Response). These requests are not sent from the public Internet, but from other Workers using a Durable Object namespace binding.
Example Project
-
The project is a simple API that allows you to create
Personobject(s), retrieve the object, update the object, and delete the object. It serves no purpose other than demonstrating the basic usage of Durable Objects. -
The Durable Object in this example is going to be a
Person. APersonobject will consist of the attributesid: number, fname: string, lname: string, age: number. - Create the project using Cloudflare Wrangler. Navigate to the directory where you would like to create the project and run
wrangler init example-durable-object-v2.Follow the prompts as follows:
- git (Y/N)
- package.json (Y)
- Create Worker at /src/index.ts (Y)
If you choose to manage it with git, create a
.gitignorefile at the root withnode_modules/as the content so git does not track thenode_modulesdirectory.Wrangler will have generated a default worker template with the following structure.
1 2 3 4 5 6 7
example-durable-object-v2/ |- src/ |- index.ts |- pacakge-lock.json |- package.json |- tsconfig.json |- wrangler.toml - Add
account_idandworkers_devto yourwrangler.tomlconfiguration file as shown below.account_idis the ID of the account associated with your zone.workers_devenables the use of *.workers.dev subdomain to test and deploy the Worker.
Refer to Wrangler Configuration documentation.
1 2 3 4 5 6
name = "example-durable-object-v2" main = "src/index.ts" compatibility_date = "2022-11-16" account_id = "<your account id>" workers_dev = true
- The API is going to use itty-router and will be structured as shown below. This post will not show the full project source code (which can be found here Github).
src/index.tsis the main entry point that importsitty-routerand defines each route.src/pages/*.tshandles the route logic for each endpoint.src/lib/person.tsis the Durable Object class.src/lib/responses.tsdefines each Response.src/html/*.jsis boilerplate code and can be ignored.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
example-durable-object-v2/ |- src/ |- html/ |- 404.js |- index.js |- lib/ |- person.ts |- responses.ts |- pages/ |- 404.ts |- create.ts |- delete.ts |- index.ts |- person.ts |- update.ts |- index.ts |- .gitignore |- README.md |- pacakge-lock.json |- package.json |- tsconfig.json |- wrangler.toml
src/index.tsis shown below. You need to installitty-routerwhich can be done by running the commandnpm install itty-router --savein the project directory. The code is well commented and explains what each block is doing.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
export interface Env { // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/ // MY_KV_NAMESPACE: KVNamespace; // // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/ PERSON: DurableObjectNamespace; // // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/ // MY_BUCKET: R2Bucket; } import home from './pages/index' import notFound from './pages/404' import person from './pages/person' import create from './pages/create' import update from './pages/update'; import del from './pages/delete'; import { Router } from 'itty-router' const router = Router() // In order for the workers runtime to find the class that implements // our Durable Object namespace, we must export it from the root module. export { Person } from './lib/person' // Routes router.get('/', (request, env, context) => home()) // Get a person router.get('/person/:id', async (request, env, context) => person(request, env, context)) // Create a person router.post('/person/:id', async (request, env, context) => create(request, env, context)) // Update a person router.put('/person/:id', async (request, env, context) => update(request, env, context)) // Delete a person router.delete('/person/:id', async (request, env, context) => del(request, env, context)) router.get('*', () => notFound()) export default { fetch: router.handle }
- In your
wrangler.tomlfile create a binding for the Durable Object as shown. Further information about bindings can be found in the links below. The binding is what allows the Worker to interact with the Durable Object.1 2
[durable_objects] bindings = [{name = "PERSON", class_name = "Person"}] src/pages/create.tsis shown below. The code is well commented and explains what each block is doing. Further information about each block can be found by following the links below. In essence, we’re creating a new instance ofPersonnamedrequest.params.id. We then get thestubusing theid. We then send therequestto thePersoninstance and wait for the response.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
import { jsonResponse } from '../lib/responses' import { jsonResponse400 } from '../lib/responses' const create = async (request: any, env: { PERSON: { idFromName: (arg0: string) => any; get: (arg0: any) => any; }; }, context: any) => { // Every unique ID refers to an individual instance of the Person class that // has its own state. `idFromName()` always returns the same ID when given the // same string as input (and called on the same class), but never the same // ID for two different strings (or for different classes). let id = env.PERSON.idFromName(request.params.id) // Construct the stub for the Durable Object using the ID. A stub is a // client object used to send messages to the Durable Object. let obj = env.PERSON.get(id) // Send a request to the Durable Object, then await its response. let resp = await obj.fetch(request) let jsonStr = await resp.json() // console.log(jsonStr) // Return the person data as json. let json = { "status": "success", "data": jsonStr, "message": "person object has been created" } return jsonResponse(JSON.stringify(json, null, 2)) } export default create
src/lib/person.tsis the Durable Object. Thefetch()handler takes therequestfrom the Worker and uses aswitchstatement to handle eachHTTPmethod. The various functions within the class handle specificPersonlogic.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
// Durable Object export class Person { state: any id: number fname: string lname: string age: number constructor(state: any, env: any) { this.state = state this.id = 0 this.fname = '' this.lname = '' this.age = 0 } // Get all person attributes getAll() { return { "id": this.id, "fname": this.fname, "lname": this.lname, "age": this.age } } // POST method logic - Set all person attributes setAll(data: any) { this.setId(data) this.setFname(data) this.setLname(data) this.setAge(data) } // PUT method logic - Update some person attributes update(data: any) { if (data.id) { this.setId(data) } if (data.fname) { this.setFname(data) } if (data.lname) { this.setLname(data) } if (data.age) { this.setAge(data) } } // DELETE method logic - Set the person attributes back to constructor values resetAll() { this.id = 0 this.fname = '' this.lname = '' this.age = 0 } // Set the person id setId(data: any) { this.id = Number(data.id) } // Set the person first name setFname(data: any) { this.fname = data.fname } // Set the person last name setLname(data: any) { this.lname = data.lname } // Set the person age setAge(data: any) { this.age = Number(data.age) } // Handle HTTP requests from clients. async fetch(request: Request) { // Durable Object storage is automatically cached in-memory, so reading the // same key every request is fast. (That said, you could also store the // value in a class member if you prefer.) let value = await this.state.storage.get("value") || 0 let data: object switch (request.method) { case "GET": value = this.getAll() break; case "POST": // Get the body data from the request data = await request.json?.() this.setAll(data) value = this.getAll() // We don't have to worry about a concurrent request having modified the // value in storage because "input gates" will automatically protect against // unwanted concurrency. So, read-modify-write is safe. For more details, // see: https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/ await this.state.storage.put("value", value) break; case "PUT": // Get the body data from the request data = await request.json?.() this.update(data) value = this.getAll() await this.state.storage.put("value", value) break; case "DELETE": // https://developers.cloudflare.com/workers/runtime-apis/durable-objects/#transactional-storage-api this.resetAll() await this.state.storage.deleteAll() break; default: return new Response("Not found", { status: 404 }); } return new Response(JSON.stringify(value)) } }
- Use
wrangler devto run a development server locally for testing. Publish the Worker to you account usingwrangler publish. You will need to append the following lines towrangler.tomlbefore publishing the Worker.1 2 3
[[migrations]] tag = "v1" # Should be unique for each entry new_classes = ["Person"]
Testing the API
Start the development server using wrangler dev in the project root directory. The server will be listening on http://localhost:8787.
I’m using the Thunder Client extension for VS Code in this section.
-
GET
https://localhost:8787/person/1should return a 400 status as there is noPersonobject.![image]()
-
POST
https://localhost:8787/person/1will create aPersonobject. You can create as manyPersonobjects as you like.![image]()
-
GET
https://localhost:8787/person/1should return a thePersonobject.![image]()
-
PUT
https://localhost:8787/person/1will update specified attributes of thePersonobject.![image]()
-
GET
https://localhost:8787/person/1should return a thePersonobject showing the updated attributes.![image]()
-
DELETE
https://localhost:8787/person/1should delete thePersonobject.![image]()
-
GET
https://localhost:8787/person/1should return a 400 status as thePersonobject was deleted.![image]()






