npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

schema-storage

v0.1.0

Published

TypeScript-first localStorage wrapper with typed keys, schema validation, and automatic migrations

Downloads

107

Readme

schema-storage

TypeScript-first wrapper around localStorage that provides typed keys, schema validation, and automatic data migrations — so persisted client-side data never breaks your app.

Installation

npm install schema-storage zod

Quick Start

import { createStorage } from "schema-storage";
import { z } from "zod";

const storage = createStorage({
  user: z.object({
    id: z.string(),
    email: z.string().email(),
  }),
  theme: z.enum(["light", "dark"]),
});

// Fully typed!
storage.set("theme", "dark"); // ✅
storage.set("theme", "blue"); // ❌ Type error

const user = storage.get("user"); // { id: string; email: string } | null
const theme = storage.getOrDefault("theme", "light"); // "light" | "dark"

Features

1. Typed Keys

No more magic strings. All keys are typed and autocompleted.

const storage = createStorage({
  settings: z.object({
    language: z.string(),
    notifications: z.boolean(),
  }),
});

storage.get("settings"); // Typed return
storage.get("unknown"); // ❌ Type error

2. Schema Validation

Every read operation validates data against your schema. Invalid or corrupted data is safely rejected.

const storage = createStorage({
  count: z.number(),
}, {
  onInvalid: "reset", // or "throw" | "ignore"
});

// If localStorage contains invalid JSON or wrong shape:
const count = storage.get("count"); // null (or throws if onInvalid: "throw")

3. Versioned Migrations

Safely evolve your data models without breaking existing users.

const storage = createStorage({
  user: {
    version: 2,
    schema: z.object({
      id: z.string(),
      email: z.string().email(),
      name: z.string(),
    }),
    migrate: {
      1: (oldData) => ({
        id: oldData.id,
        email: "[email protected]",
        name: "User",
      }),
    },
  },
});

When get("user") is called:

  1. Detects stored version (1)
  2. Applies migration from 1 → 2
  3. Validates final result
  4. Persists upgraded version automatically

4. Storage-Agnostic

Use any storage backend with the same API.

import { createStorage, MemoryStorage } from "schema-storage";

// Default: localStorage
const storage1 = createStorage({ ... });

// sessionStorage
const storage2 = createStorage({ ... }, { driver: sessionStorage });

// Memory (SSR-safe, testing)
const storage3 = createStorage({ ... }, { driver: new MemoryStorage() });

API

createStorage<T>(schema, options?)

Creates a typed storage instance.

Parameters:

  • schema: Object mapping keys to Zod schemas or versioned schemas
  • options:
    • driver?: StorageDriver - Storage backend (default: localStorage)
    • onInvalid?: "throw" | "reset" | "ignore" - Behavior for invalid data (default: "ignore")
    • prefix?: string - Key prefix for namespacing

Returns: SafeStorage<T>

Storage Methods

get<K>(key: K): T[K] | null

Reads and validates a value. Returns null if missing or invalid.

set<K>(key: K, value: T[K]): void

Writes a value with validation. Throws if validation fails and onInvalid: "throw".

remove(key: keyof T): void

Removes a key and its metadata.

clear(): void

Clears all storage (or all prefixed keys if using a prefix).

has(key: keyof T): boolean

Checks if a key exists.

getOrDefault<K>(key: K, defaultValue: T[K]): T[K]

Gets a value or returns the default if missing/invalid.

Examples

Basic Usage

import { createStorage } from "schema-storage";
import { z } from "zod";

const storage = createStorage({
  todos: z.array(z.object({
    id: z.string(),
    text: z.string(),
    completed: z.boolean(),
  })),
  filter: z.enum(["all", "active", "completed"]),
});

storage.set("todos", [
  { id: "1", text: "Learn schema-storage", completed: false },
]);

const todos = storage.get("todos"); // Fully typed!

With Migrations

const storage = createStorage({
  preferences: {
    version: 3,
    schema: z.object({
      theme: z.enum(["light", "dark", "auto"]),
      fontSize: z.number().min(12).max(24),
      language: z.string(),
    }),
    migrate: {
      1: (old) => ({
        theme: old.theme ?? "light",
        fontSize: 16,
        language: "en",
      }),
      2: (old) => ({
        ...old,
        theme: old.theme === "system" ? "auto" : old.theme,
      }),
    },
  },
});

Custom Storage Driver

import { createStorage, type StorageDriver } from "schema-storage";

class CustomStorage implements StorageDriver {
  private data = new Map<string, string>();

  getItem(key: string): string | null {
    return this.data.get(key) ?? null;
  }

  setItem(key: string, value: string): void {
    this.data.set(key, value);
  }

  removeItem(key: string): void {
    this.data.delete(key);
  }

  clear(): void {
    this.data.clear();
  }

  key(index: number): string | null {
    return Array.from(this.data.keys())[index] ?? null;
  }

  get length(): number {
    return this.data.size;
  }
}

const storage = createStorage({ ... }, { driver: new CustomStorage() });

Why schema-storage?

localStorage problems:

  • Everything is a string (manual JSON parsing)
  • No type safety
  • No validation
  • No migration strategy

schema-storage solves:

  • ✅ Typed keys with autocomplete
  • ✅ Automatic JSON handling
  • ✅ Runtime validation with Zod
  • ✅ Versioned migrations
  • ✅ Storage-agnostic design

License

MIT