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 parameter5 RateLimit: "signedIp" // one instance per client6 },7 routes,8 exports9});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 keys4await store.get("profile"); // read one key5await store.delete("profile"); // or an array of keys6await store.deleteAll(); // wipe this instance7await 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 62const current = await state("Counter").get("count");3await state("Counter").put({ count: current + 1 });4 5// Safe6const 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.


