Read, write, and expire values
Read and write typed values
Serialize values deliberately, handle missing keys, and use metadata for small information needed during reads or lists.
8 minute lesson
KV stores byte or string values. Everything else — objects, numbers, booleans — is a serialization decision you make and must keep consistent between the writer and the reader.
For structured data, write JSON and read it back typed:
await env.SETTINGS.put(
'tenant:123:settings',
JSON.stringify({ theme: 'dark', locale: 'it' })
)
const settings = await env.SETTINGS.get('tenant:123:settings', 'json')
Passing json as the type makes get parse the value for you. Use get with a type such as json only when the stored format is controlled — when your own code wrote the value. If parsing fails because someone stored malformed data, the read throws, so a value written by hand in the dashboard can break a production read path.
Missing keys are normal
get returns null for a missing key. Not an error, not an empty string. Handle it explicitly:
const settings = await env.SETTINGS.get('tenant:123:settings', 'json')
if (settings === null) {
return defaults()
}
Decide what “missing” means for each key — fall back to defaults, treat it as logged out, recompute. Code that assumes the key always exists works right up to the first new tenant.
Key names are a design surface
Choose a key format that includes tenant and purpose, such as tenant:123:settings. A consistent format makes prefix listings useful and keeps unrelated data from colliding.
Two safety rules. Validate sizes: keys are limited to 512 bytes and values to 25 MiB, and a user-supplied fragment can blow past the key limit. And never let an untrusted user select another tenant’s prefix — if the tenant ID in the key comes from the request body instead of the verified identity, one customer can read another’s data.
Metadata rides along
A write can attach small JSON metadata, and getWithMetadata returns both:
await env.SETTINGS.put('tenant:123:settings', body, {
metadata: { version: 3, updatedBy: 'flavio' },
})
const { value, metadata } = await env.SETTINGS.getWithMetadata('tenant:123:settings', 'json')
Metadata also comes back in list results without reading each value, which makes it good for small per-key facts. It is not a relational query system — you cannot filter or join on it.
Now write and read one typed settings record, then test two failure paths: malformed JSON stored under the key, and a missing key.
Lesson completed