Skip to content

Quick start

Each kit installs independently — pick only what you need. The peer dependencies below are kept as peers (not bundled) so you control their versions and so unused features cost nothing.

Persisted Zustand stores with versioned migrations. Install zustand alongside it: store-kit declares Zustand as a peer (>=4.5.0 <6.0.0) so your app picks the patch level, and any other library that uses Zustand shares the same instance.

Terminal window
pnpm add @arshad-shah/store-kit zustand

Typed fetch client with retries, timeouts, and an error class hierarchy. Both peers are optional (declared via peerDependenciesMeta): install nothing extra if you only use the core client. Add react if you’ll import the useFetch / useMutation hooks from @arshad-shah/fetch-kit/react, and zod if you want runtime schema validation on responses (the validator is a parse interface, so any compatible library works — zod is just the most common).

Terminal window
# core client only
pnpm add @arshad-shah/fetch-kit
# add `react zod` if you'll use the hooks or schema validation

Structured logger with pluggable transports. Zero peers, zero runtime deps — the rarest profile in the JS ecosystem. Transports (console, http, file, datadog) are subpath imports under @arshad-shah/log-kit/transports/*, tree-shaken to only what you actually import.

Terminal window
pnpm add @arshad-shah/log-kit

Schema-validated config loaded from env vars, files, and remote sources. zod is a required peer because loadConfig returns Zod-inferred types — you can pass any object with a .parse() method at runtime, but the static types assume Zod’s inference. @arshad-shah/log-kit is an optional peer: install it if you want config-kit to surface diagnostics (source resolution, validation failures) through your existing logger instead of console.

Terminal window
pnpm add @arshad-shah/config-kit zod
import { createStore } from "@arshad-shah/store-kit";
export const useCounter = createStore({
name: "counter",
initial: { count: 0 },
actions: (set) => ({
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
}),
persist: { storage: "local", version: 1 },
});
function Counter() {
const count = useCounter((s) => s.count);
const { increment, decrement } = useCounter.getState();
return (
<>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</>
);
}

The state survives page refreshes. Bump version and add a migrate map when the shape changes.

import { createClient } from "@arshad-shah/fetch-kit";
export const api = createClient({
baseUrl: "https://api.example.com",
timeout: 10_000,
retry: { attempts: 3, backoff: "exponential" },
auth: () => localStorage.getItem("token"),
});
const user = await api.get<User>("/users/me");
import { useFetch } from "@arshad-shah/fetch-kit/react";
function Profile() {
const { data, error, loading, refetch } = useFetch<User>(api, "/users/me");
if (loading) return <Spinner />;
if (error) return <button onClick={refetch}>Retry</button>;
return <h1>{data!.name}</h1>;
}
import { createLogger } from "@arshad-shah/log-kit";
import { consoleTransport } from "@arshad-shah/log-kit/transports/console";
export const log = createLogger({
level: "info",
transports: [consoleTransport({ pretty: process.env.NODE_ENV !== "production" })],
});
const end = log.mark("server.boot");
await startServer();
end({ port: 3000 });
import { z } from "zod";
import { loadConfig, processEnvSource, dotenvFileSource } from "@arshad-shah/config-kit";
export const config = await loadConfig({
schema: z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
}),
sources: [dotenvFileSource(".env"), processEnvSource()],
});
config.PORT; // number, fully typed

That’s the whole API surface for typical use. Read the package overviews next: