Home Cloudflare Workers Durable Objects example
Post
Cancel

Cloudflare Workers Durable Objects example

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

  1. The project is a simple API that allows you to create Person object(s), retrieve the object, update the object, and delete the object. It serves no purpose other than demonstrating the basic usage of Durable Objects.

  2. The Durable Object in this example is going to be a Person. A Person object will consist of the attributes id: number, fname: string, lname: string, age: number.

  3. 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 .gitignore file at the root with node_modules/ as the content so git does not track the node_modules directory.

    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
    
  4. Add account_id and workers_dev to your wrangler.toml configuration file as shown below.
    • account_id is the ID of the account associated with your zone.
    • workers_dev enables 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
    
  5. 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.ts is the main entry point that imports itty-router and defines each route.
    • src/pages/*.ts handles the route logic for each endpoint.
    • src/lib/person.ts is the Durable Object class.
    • src/lib/responses.ts defines each Response.
    • src/html/*.js is 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
      
  6. src/index.ts is shown below. You need to install itty-router which can be done by running the command npm install itty-router --save in 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
     }
    
  7. In your wrangler.toml file 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"}]
    
  8. src/pages/create.ts is 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 of Person named request.params.id. We then get the stub using the id. We then send the request to the Person instance 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
    
  9. src/lib/person.ts is the Durable Object. The fetch() handler takes the request from the Worker and uses a switch statement to handle each HTTP method. The various functions within the class handle specific Person logic.
    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))
         }
     }
    
  10. Use wrangler dev to run a development server locally for testing. Publish the Worker to you account using wrangler publish. You will need to append the following lines to wrangler.toml before 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.

  1. GET https://localhost:8787/person/1 should return a 400 status as there is no Person object. image

  2. POST https://localhost:8787/person/1 will create a Person object. You can create as many Person objects as you like. image

  3. GET https://localhost:8787/person/1 should return a the Person object. image

  4. PUT https://localhost:8787/person/1 will update specified attributes of the Person object. image

  5. GET https://localhost:8787/person/1 should return a the Person object showing the updated attributes. image

  6. DELETE https://localhost:8787/person/1 should delete the Person object. image

  7. GET https://localhost:8787/person/1 should return a 400 status as the Person object was deleted. image

This post is licensed under CC BY 4.0 by the author.