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

@ofrusch/kita

v0.1.1

Published

A frontend ORM and reactive state management framework for Vue 3 — typed models, HTTP-backed stores, and a model-store registry inspired by Ember Data.

Readme

kita

A frontend ORM and reactive state management framework for Vue 3 — strongly-typed models, HTTP-backed stores, and a model-store registry inspired by Ember Data.

If you've used a backend ORM (Active Record, Sequelize, Prisma, AdonisJS Lucid, Django ORM…), kita gives you the same mental model on the client: model classes that mirror API resources, with stores that handle fetching, caching, mutating, and relating records. The shape your backend ORM serializes is the same shape your frontend model consumes — no hand-rolled fetcher/setter glue between them.

  • Frontend ORMModel and AsyncModel classes with .save() / .delete() / relations
  • Pairs cleanly with your backend ORM — API endpoints exposing typed resources land directly as typed records, no wrapper code per route
  • Stage 3 decorators — modern @reactive() accessor syntax, no experimentalDecorators flag
  • HTTP-agnostic — duck-typed HttpClient interface; works with axios, ky, redaxios, or any custom client
  • Built-in helpers — request deduplication, query caching, pagination, optimistic updates
  • Opt-in SWR — stale-while-revalidate semantics available as a separate base class
  • Vue DevTools — inspect every store and record in your app

Why an ORM on the frontend?

Most Vue state libraries give you reactive boxes (refs, stores, signals) and leave domain modeling to you — every component reaches into an axios client, maps raw JSON, and patches into a flat store. That works for small apps; it doesn't scale.

Kita takes the other approach, borrowed from Ember Data: treat the client as a thin mirror of your backend's data layer.

  • Models describe what a resource is (its fields, computed properties, relations to other models)
  • Stores describe how to fetch, cache, and mutate that resource
  • The registry wires models to their stores automatically — UserModel.create({ id: "1" }) knows to push itself into UserStore, and userModel.save() knows to POST to /users/

The result: when your backend already enforces a model (via Lucid, Prisma, etc.), kita lets you describe the same model on the client, in ~10 lines per resource. Adding a new field to a UserModel on both sides is a one-line change in two files.

Influences

  • Ember Data — the model + store + registry pattern, automatic record identity, and the design goal of treating the client as an ORM-shaped cache of your backend
  • axios — the HttpClient interface is axios-compatible by design
  • SWR / TanStack Query — the opt-in AsyncStoreSWR borrows their stale-while-revalidate semantics

Kita is not a fork of Ember Data — it's a much smaller, Vue-native take on the same ideas. No JSON:API requirement, no Ember runtime, no opinions about your bundler.

Installation

pnpm add @ofrusch/kita
# or
npm install @ofrusch/kita
# or
yarn add @ofrusch/kita

Peer dependency: vue@^3.0.0.

Quick start

1. Define a model

import { AsyncModel, registerModel } from "@ofrusch/kita";

export class UserModel extends AsyncModel {
  static readonly id = "users";
  static {
    registerModel(this);
  }

  declare email: string;
  declare name: string;
}

2. Define a store

import { AsyncStore, reactive } from "@ofrusch/kita";
import { UserModel } from "./UserModel";

export class UserStore extends AsyncStore<UserModel> {
  static readonly id = "users";

  @reactive()
  accessor currentUser: UserModel | null = null;

  async login(email: string, password: string) {
    const res = await this.client.post("/auth/login/", { email, password });
    this.currentUser = UserModel.create(res.data);
    return this.currentUser;
  }
}

3. Wire up the application store

import axios from "axios";
import { ApplicationStore, createAndRegisterStore } from "@ofrusch/kita";
import { UserStore } from "./stores/UserStore";

class AppStore extends ApplicationStore {
  declare readonly users: UserStore;
}

const client = axios.create({ baseURL: "/api" });

const { appStore, useStore } = createAndRegisterStore(AppStore, [UserStore], client);

export default appStore;
export { useStore };

4. Install on the Vue app

import { createApp } from "vue";
import App from "./App.vue";
import appStore from "./stores/application-store";

const app = createApp(App);
app.use(appStore);
app.mount("#app");

5. Use in components

import { computed } from "vue";
import { useStore } from "./stores/application-store";

const { users } = useStore();

const currentUser = computed(() => users.currentUser);

async function handleLogin() {
  await users.login("[email protected]", "password");
}

Pairing with a backend ORM

A typical setup with a backend Lucid/Prisma/Sequelize model and a matching kita model:

// backend (AdonisJS Lucid)
export default class User extends BaseModel {
  @column({ isPrimary: true }) declare id: string;
  @column() declare email: string;
  @column() declare displayName: string;
  @hasMany(() => Post) declare posts: HasMany<typeof Post>;
}

// frontend (kita)
export class UserModel extends AsyncModel {
  static readonly id = "users";
  static { registerModel(this); }

  declare email: string;
  declare displayName: string;

  get posts() {
    return this.stores.posts.records.filter((p) => p.userId === this.id);
  }
}

The route GET /users/:id returning the Lucid model's .toJSON() lands as a fully-typed UserModel instance via users.findRecord(id). Relations on the frontend resolve via this.stores. Saving works in reverse: user.email = "new@..."; await user.save(); issues a PUT /users/:id with the serialized fields.

The @reactive decorator

@reactive() converts a class field into a Vue-reactive property backed by a ref(). Use the Stage 3 accessor keyword and provide an initial value:

import { reactive, Store } from "@ofrusch/kita";

class FilterStore extends Store<FilterModel> {
  @reactive()
  accessor query: string = "";

  @reactive()
  accessor selectedTags: string[] = [];
}

Reads and writes go through Vue's reactivity system, so reactive properties trigger re-renders, computed, and watch exactly like a normal ref.

The decorator argument is ignored — initial values come from the field initializer (= "", = [], etc.). The argument is kept for backwards compatibility with the legacy @reactive(value) syntax.

HTTP client

AsyncStore and ApplicationStore accept any client that matches the HttpClient interface:

export interface HttpClient {
  get<T>(url: string, config?: { params?: Record<string, unknown> }): Promise<{ data: T }>;
  post<T>(url: string, data?: unknown): Promise<{ data: T }>;
  put<T>(url: string, data?: unknown): Promise<{ data: T }>;
  delete<T>(url: string): Promise<{ data: T }>;
}

AxiosInstance satisfies this interface out of the box — no adapter required. Any other client (ky, redaxios, native fetch wrapper) works as long as it matches the shape.

Utilities

All utilities are importable from @ofrusch/kita.

withOptimisticUpdate

Update the UI immediately and roll back on server error:

import { withOptimisticUpdate } from "@ofrusch/kita";

await withOptimisticUpdate(
  () => {
    const snapshot = { votes: item.votes };
    item.votes += 1;
    return snapshot;
  },
  () => api.post(`/items/${item.id}/vote`),
  (snapshot) => {
    item.votes = snapshot.votes;
  },
);

AsyncStore also exposes optimisticCreate, optimisticUpdate, and optimisticDelete for record-level mutations.

RequestTracker

Deduplicate concurrent in-flight requests:

import { RequestTracker } from "@ofrusch/kita";

const tracker = new RequestTracker();

const user = await tracker.dedupe(`user-${id}`, () => api.get(`/users/${id}`));

QueryCache

TTL-based cache for list queries. AsyncStore.findRecords uses this internally and auto-invalidates on _createRecord / _updateRecord / _deleteRecord. Call store.invalidateQueries() after any custom mutation that changes the set of records.

import { QueryCache } from "@ofrusch/kita";

const cache = new QueryCache<Item>(60_000); // 1 minute TTL
cache.set({ q: "hello" }, results);
cache.get({ q: "hello" });

PaginatedQuery

Page-tracking helper for infinite scroll:

import { PaginatedQuery } from "@ofrusch/kita";

const query = new PaginatedQuery(async (page) => {
  const { records, meta } = await store.findRecords({ page });
  return { records, meta };
});

const firstPage = await query.loadMore();
query.hasMore; // boolean
query.isLoading; // boolean

AsyncStore.createPaginatedQuery(params) returns a pre-wired instance.

Opt-in stale-while-revalidate

If you want SWR semantics on findRecord (return cached, refetch in background after staleTime), extend AsyncStoreSWR instead of AsyncStore:

import { AsyncStoreSWR } from "@ofrusch/kita";

class UserStore extends AsyncStoreSWR<UserModel> {
  static readonly id = "users";
}

// returns instantly if cached and < 30s old, else awaits the fetch
await users.findRecord("u-1", {}, { staleTime: 30_000 });

// returns instantly, kicks off a background refetch
await users.findRecord("u-1", {}, { revalidate: true });

The base AsyncStore has a smaller surface and simpler typing — pick AsyncStoreSWR only if you need freshness tracking.

API reference

Core

| Export | Purpose | | ------------------------ | --------------------------------------------------------- | | ApplicationStore | Container for domain stores; Vue plugin | | createStore | Factory for an app store without registering child stores | | createAndRegisterStore | Factory that also registers child stores | | Store<T> | Base sync store | | AsyncStore<T> | Base HTTP-backed store | | AsyncStoreSWR<T> | Opt-in stale-while-revalidate variant of AsyncStore | | AbstractStore<T> | Base of Store and AsyncStore | | Model | Base sync model | | AsyncModel | Base HTTP-backed model with save() / delete() | | AbstractModel | Base of Model and AsyncModel | | registerModel | Register a model class with the global registry | | reactive | @reactive() Stage 3 accessor decorator | | dataStorePlugin | Vue DevTools plugin (auto-installed by ApplicationStore) |

Utilities

| Export | Purpose | | ---------------------- | ------------------------------------ | | RequestTracker | Concurrent-request deduplication | | QueryCache | TTL-based query cache | | PaginatedQuery | Page-tracking for loadMore UIs | | withOptimisticUpdate | Optimistic update with auto-rollback |

Types

| Export | Purpose | | -------------------- | ---------------------------------------- | | HttpClient | Minimal HTTP client interface | | HttpResponse<T> | { data: T } | | HttpRequestConfig | { params?: Record<string, unknown> } | | FindRecordOptions | SWR options (AsyncStoreSWR) | | FindRecordsOptions | findRecords cache options | | PaginationMeta | Pagination response metadata | | PaginatedResult<T> | { records: T[], meta: PaginationMeta } |

Requirements

  • Vue 3
  • TypeScript 5.0+ (for native Stage 3 decorators)
  • Node 18+

License

MIT © Owen Carpenter