Every Nuxt app has the same problem.
For every API resource — users, posts, products, orders — the same code gets written:
- Define a TypeScript interface
- Create reactive state (
ref,reactive) - Create a loading ref
- Create an error ref
- Write a fetch function with try/catch/finally
- Repeat for create, update, delete
- Repeat for the next resource
Harlemify is an open-source Nuxt module by Diphyx that eliminates this repetition. It's built on top of Harlem, a powerful and extensible state management library for Vue 3 — so the reactive store layer is solid and battle-tested.
Define your data shape once, get everything else for free
Harlemify generates typed stores from Zod schemas. Here's what a full CRUD store looks like:
import { createStore, shape, ModelOneMode, ModelManyMode } from "@diphyx/harlemify/runtime";
const userShape = shape((factory) => {
return {
id: factory.number().meta({
identifier: true,
}),
name: factory.string(),
email: factory.email(),
};
});
export const userStore = createStore({
name: "users",
model({ one, many }) {
return {
current: one(userShape),
list: many(userShape),
};
},
view({ from }) {
return {
user: from("current"),
users: from("list"),
};
},
action({ api }) {
return {
list: api.get(
{
url: "/users",
},
{ model: "list", mode: ModelManyMode.SET },
),
get: api.get(
{
url: "/users/:id",
},
{ model: "current", mode: ModelOneMode.SET },
),
create: api.post(
{
url: "/users",
},
{ model: "list", mode: ModelManyMode.ADD },
),
delete: api.delete(
{
url: "/users/:id",
},
{ model: "list", mode: ModelManyMode.REMOVE },
),
};
},
});
No separate type definitions. No manual loading/error refs. No try/catch boilerplate.
What comes out of this single definition
Typed models — current holds a single user, list holds a collection. Both are fully typed from the Zod shape.
Reactive views — user and users are computed refs that update whenever the underlying model changes. Powered by Harlem's reactive store engine under the hood.
API actions with auto-commit — Each action knows which model to update and how (SET replaces, ADD appends, REMOVE deletes by identifier).
Automatic status tracking — Every action exposes loading, error, and status without writing a single ref.
Using it in components
<script setup>
const { execute, loading, error } = useStoreAction(userStore, "list");
const { data: users } = useStoreView(userStore, "users");
await execute();
</script>
<template>
<p v-if="error">{{ error.message }}</p>
<ul v-else-if="!loading">
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>
Three composables cover everything:
-
useStoreAction— execute actions, track loading/error/status -
useStoreView— access reactive computed state -
useStoreModel— mutate state directly (set, patch, add, remove, reset)
Beyond basic state management
Concurrency control
Every action can be configured with a concurrency strategy:
- BLOCK — throw if the action is already running
- SKIP — return the existing promise
- CANCEL — abort the previous call, start a new one
- ALLOW — run both in parallel
No more debouncing hacks or manual AbortController wiring.
Record collections
For grouped data (e.g., users by team), use record mode:
model({ many }) {
return {
byTeam: many(userShape, { kind: ModelManyKind.RECORD }),
};
},
This gives you { teamA: [...users], teamB: [...users] } with typed CRUD operations per key.
Handler actions
Not every action is an API call. Custom logic gets first-class support:
action({ handler }) {
return {
sortByName: handler(async ({ model }) => {
const sorted = [...model.list].sort((a, b) => a.name.localeCompare(b.name));
model.list = sorted;
}),
};
},
Handlers have full access to models and views, with the same status tracking as API actions.
SSR with automatic hydration
Harlemify uses Harlem's SSR plugin (@harlem/plugin-ssr) under the hood. Server-rendered state is automatically hydrated on the client — no manual transfer or serialization needed.
How it compares
| Manual / Pinia | Harlemify | |
|---|---|---|
| Schema | Separate TypeScript interfaces | Zod shape — types + validation |
| API calls | Manual fetch + state updates | Declarative with auto-commit |
| Loading/error | Manual refs per action | Automatic per action |
| Concurrency | Not built-in | Block, Skip, Cancel, Allow |
| Collections | Manual array management | Built-in modes (SET, ADD, PATCH, REMOVE) |
| SSR | Plugin required | Built-in via Harlem SSR |
| Lines per resource | ~50-60 | ~15-20 |
Harlemify is not a Pinia replacement. Pinia is great for general-purpose client state. Harlemify is built on top of Harlem and optimized for API-driven data — the CRUD resources that make up 80% of most apps.
Try it
npm install @diphyx/harlemify
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@diphyx/harlemify"],
});
Feedback and contributions are welcome — star the repo if it's useful, or open an issue for anything that's missing.
Top comments (0)