Gives localStorage superpowers. Handles serialization of rich types, key expiration, namespacing, and schema validation — so you don't have to.
- Store anything: Stores
Set,Map,Date,RegExp,BigInt, circular references, and more using devalue - TTL / expiration: Set a
ttlin milliseconds or an absoluteexpiresAttimestamp. Expired items are cleaned up lazily on read - Namespacing: Isolate keys with a configurable
prefixandseparator - Schema validation: Validate retrieved values against any Standard Schema (Zod, Valibot, ArkType, etc.)
- Use any
Storagebackend: Works withlocalStorage,sessionStorage, or anyStorage-compatible implementation. An in-memory storage implementation is provided as well - ESM and CJS: Tree-shakeable dual builds with full TypeScript types
npm install greatstorageStore and retrieve objects without the JSON.stringify dance. You're welcome.
// lib/storage.js
import { createStorage } from 'greatstorage';
// Create an app-wide singleton instance.
const storage = createStorage();
export storage;// app.js
import { storage } from './lib/storage.js';
storage.setItem('user', { name: 'Alice', age: 30 });
storage.getItem('user'); // { name: 'Alice', age: 30 }Yes, localStorage can finally handle a Set, or any data type you throw at it. It only took the entire JavaScript ecosystem to get here.
Uses devalue by default, but you can bring your own serializer.
storage.setItem('tags', new Set(['a', 'b', 'c']));
storage.getItem('tags'); // Set {'a', 'b', 'c'}
storage.setItem('metadata', new Map([['key', 'value']]));
storage.getItem('metadata'); // Map {'key' => 'value'}
storage.setItem('date', new Date('2025-01-01'));
storage.getItem('date'); // Date 2025-01-01T00:00:00.000ZStore data temporarily. Like Snapchat, but for your storage keys.
Note: The data is not immediately removed after expiry timing, it's only removed on next access.
// Expires in 60 seconds
storage.setItem('token', 'abc123', { ttl: 60_000 });
// Expires at a specific date
storage.setItem('session', { id: 1 }, { expiresAt: new Date('2025-12-31') });
// Expired items return null
storage.getItem('token'); // null (after 60s)Because your keys deserve their own personal space, away from whatever chaos other libraries left behind.
const appStorage = createStorage({ prefix: 'my-vibe-coded-app' });
appStorage.setItem('theme', 'dark'); // stored as "my-vibe-coded-app:theme"
appStorage.getItem('theme'); // 'dark'
localStorage.getItem('theme'); // null
// clear() only removes keys within the namespace
appStorage.clear();The usual housekeeping. Someone has to take out the trash.
storage.has('user'); // true
storage.removeItem('user');
storage.has('user'); // false
storage.clear(); // remove all greatstorage entries
storage.clearExpired(); // remove only expired entriesTypeScript can't read localStorage at compile time (yet), but you can at least pretend your data is typed.
interface User {
name: string;
age: number;
}
const user = storage.getItem<User>('user');
// user is typed as User | null
storage.getOrInit<User>('user', () => ({ name: 'Alice', age: 30 }));
storage.updateItem<User>('user', (current) => ({
...current!,
age: current!.age + 1,
}));However, the true safe way is to validate with a schema during read.
Trust no one — especially not browser storage is open to tampering by users. Validate with any libraries that support Standard Schema.
import { z } from 'zod';
const UserSchema = z.object({ name: z.string(), age: z.number() });
// Returns typed value if valid, null if validation fails
const user = storage.getItem('user', { schema: UserSchema });Pass any Storage-compatible backend. Use sessionStorage for tab-scoped data, or createMemoryStorage() for tests and server-side rendering.
import { createStorage, createMemoryStorage } from 'greatstorage';
const storage = createStorage({
storage: typeof window === 'undefined' ? createMemoryStorage() : undefined,
});Don't like devalue? Bring your own stringify/parse and we won't judge. Much.
import superjson from 'superjson';
const storage = createStorage({
serializer: { stringify: superjson.stringify, parse: superjson.parse },
});Because getItem and setItem weren't enough, here are some bonus methods you didn't know you needed.
Get the value if it exists, or writes to storage if it doesn't. Either way, you're getting something back.
const prefs = storage.getOrInit('prefs', () => ({
theme: 'light',
lang: 'en',
}));Read-modify-write in one call. Three separate statements was apparently too much work even when AI is writing all the code.
storage.updateItem('count', (current) => (current ?? 0) + 1);Creates a new storage instance. All options are optional.
| Option | Type | Default | Description |
|---|---|---|---|
prefix |
string |
— | Key prefix for namespacing |
separator |
string |
":" |
Separator between prefix and key |
storage |
Storage |
localStorage |
Underlying Storage backend |
serializer |
Serializer |
devalue |
Custom serializer with stringify and parse methods |
Returns a GreatStorage instance with the following methods:
Retrieves and deserializes a value. Returns null if the key is missing, expired, or fails schema validation.
Pass { schema } to validate the value against a Standard Schema.
Serializes and stores a value. Options:
| Option | Type | Description |
|---|---|---|
ttl |
number |
Time-to-live in milliseconds |
expiresAt |
Date | number |
Absolute expiration time |
ttl and expiresAt cannot be used together.
Returns the existing value for key, or calls factory() to create, store, and return a new value. Accepts the same options as setItem.
Calls updater(currentValue) where currentValue is the existing value (or null), stores the result, and returns it. Accepts the same options as setItem.
Removes a single key.
Returns true if the key exists and is not expired.
Returns the key at the given index among non-expired entries, or null.
Removes all greatstorage entries in the current namespace.
Removes only expired entries in the current namespace.
The number of non-expired entries in the current namespace.
Returns an in-memory Storage implementation. Useful for testing or server-side usage.
- store2 by Nathan Bubna: feature-rich
localStoragewrapper with namespacing and plugins - store.js by Marcus Westin: cross-browser
localStoragewrapper with fallback plugins - unstorage by UnJS: universal key-value storage with pluggable drivers (memory, filesystem, Redis, etc.)
- storage-box by Shahrad Elahi: simple
localStoragewrapper with TTL support - lscache by Pamela Fox:
localStoragewrapper with memcached-inspired expiration
MIT