Apiker stores data in Durable Objects. Each object is a small, single-threaded store with its own persistent storage, and each instance of it is addressed by an id — one per user, per IP, or a single shared one.

const handler = async ({ state, matches }) => { await state("Users", matches.params.id).put({ profile: { name: "Ada" } }); return res(await state("Users", matches.params.id).get("profile")); };

Choosing the instance

state(objectName, objectId) takes the object class and the instance id. Omit the name and you get Common; omit the id and Apiker derives one from objectStateMapping.

1apiker.init({
2 objects: ["Common", "Users", "RateLimit"],
3 objectStateMapping: {
4 Users: "userId", // the :userId route parameter
5 RateLimit: "signedIp" // one instance per client
6 },
7 routes,
8 exports
9});

A mapping value is one of:

  • signedIp — a signed hash of the caller's IP, stable between requests and safe to store
  • clientId — a hash of IP and User-Agent, so one browser gets one instance
  • ip — the raw client IP
  • a route parameter name — the value of that parameter on the matched route
  • any other string — used literally, giving one shared instance

RateLimit and Logs default to signedIp, and Bans to userId, so those features work without configuration.

Reading and writing

1const store = state("Users", userId);
2
3await store.put({ profile, updatedAt: Date.now() }); // write one or more keys
4await store.get("profile"); // read one key
5await store.delete("profile"); // or an array of keys
6await store.deleteAll(); // wipe this instance
7await store.list({ prefix: "log:", limit: 50, reverse: true });

list returns an object keyed by storage key, not an array, and accepts prefix, start, startAfter, end, reverse, limit and noCache.

Counters

Read-then-write loses updates when two requests arrive together. increment applies the change atomically and returns the new totals.

1// Racy: two concurrent requests can both read 5 and both write 6
2const current = await state("Counter").get("count");
3await state("Counter").put({ count: current + 1 });
4
5// Safe
6const totals = await state("Counter").increment({ increments: { count: 1 } });

increment also maintains a fixed-size ring buffer, which keeps a rotating sample of recent events without the storage growing:

await state("RateLimit").increment({ increments: { requests: 1 }, ring: { prefix: "sample_", size: 100, from: "requests", value: { at: Date.now() } } });

How instances behave

  • The same name and id always reach the same instance, and its data survives deployments.
  • Cloudflare serialises concurrent requests to one instance, so there is no locking to do.
  • Instances are independent — partition by user or IP so one hot key cannot become a bottleneck.
Every object you address must appear in the objects list passed to apiker.init, otherwise there is no binding to route the call to.