Skip to content

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

License

Notifications You must be signed in to change notification settings

Justinkarso/schema-storage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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: "unknown@example.com",
        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

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published