@noj-tech/data-layer
v1.0.2
Published
A lightweight, framework-agnostic data layer for modern JavaScript and TypeScript applications.
Maintainers
Readme
@noj-tech/data-layer
A lightweight, framework-agnostic data layer for modern JavaScript and TypeScript applications.
Features
- Lightweight and framework-agnostic
- TypeScript-first
- Modular and extensible architecture
- Built-in caching support
- Multiple cache execution strategies
- Independent from Axios, Fetch, or any specific HTTP client
- Supports Vue, React, Nuxt, Node.js, and other JavaScript environments
- Unit tested with Vitest
Installation
npm install @noj-tech/data-layerBasic Usage
Create a data layer:
import { createLayer } from "@noj-tech/data-layer";
const layer = createLayer();The layer can execute any asynchronous operation through the run API.
const result = await layer.run({
execute: async () => {
return fetchUsers();
},
});The data layer does not care how fetchUsers() is implemented.
It can use Fetch, Axios, GraphQL, WebSocket clients, database calls, or any custom data source.
Architecture
The package provides a small runtime for managing data-related operations.
Application
│
▼
┌─────────────────────┐
│ Data Layer │
├─────────────────────┤
│ Runner │
│ Registry │
│ Modules │
│ Cache │
└─────────────────────┘
│
├── API / HTTP
├── Cache
├── Storage
└── Custom ModulesThe core does not depend on a specific framework, HTTP client, or storage implementation.
Cache
Caching is a first-class capability of the data layer.
A cache store can be registered on the layer:
const layer = createLayer();
layer.register("cache", cache);The registered cache is then available to the runner during data operations.
A cache store implements the following interface:
export interface CacheStore {
has(key: string): Promise<boolean>;
get<T>(key: string): Promise<T | undefined>;
set<T>(key: string, value: T): Promise<void>;
remove(key: string): Promise<boolean>;
clear(): Promise<void>;
}The package currently includes cache stores such as:
- MemoryStore
- LocalStorageStore
- IndexedDBStore
Cache Strategies
The runner supports multiple execution strategies.
Available Strategies
cache-firstnetwork-firstcache-onlynetwork-onlystale-while-revalidate
Cache-First
cache-first checks the cache before executing the underlying operation.
Request
│
▼
Check Cache
│
├── Hit ──────► Return Cached Data
│
└── Miss
│
▼
Execute Operation
│
▼
Store Result
│
▼
Return DataUsage:
const result = await layer.run({
key: "users",
cache: {
strategy: "cache-first",
},
execute: () => fetchUsers(),
});Behavior:
- Check the cache.
- If cached data exists, return it immediately.
- If there is no cached data, execute the operation.
- Store the result in the cache.
- Return the result.
This strategy is useful when cached data should be preferred and network requests should only happen when necessary.
Network-First
network-first tries the underlying operation first.
If the operation succeeds, the result is stored in the cache.
If the operation fails, the runner attempts to return cached data.
Request
│
▼
Execute Operation
│
├── Success ─────► Store Result ─────► Return Data
│
└── Failure
│
▼
Check Cache
│
├── Hit ─────► Return Cached Data
│
└── Miss ────► Throw ErrorUsage:
const result = await layer.run({
key: "users",
cache: {
strategy: "network-first",
},
execute: () => fetchUsers(),
});Behavior:
- Execute the operation.
- If successful, store the result in the cache.
- Return the fresh result.
- If the operation fails, check the cache.
- If cached data exists, return it.
- If no cached data exists, rethrow the original error.
This strategy is useful when fresh data is preferred but cached data can act as a fallback.
Cache-Only
cache-only never executes the underlying operation when a cache key and cache are available.
Request
│
▼
Check Cache
│
├── Hit ──────► Return Cached Data
│
└── Miss ─────► Throw Cache Miss ErrorUsage:
const result = await layer.run({
key: "users",
cache: {
strategy: "cache-only",
},
execute: () => fetchUsers(),
});If the cache does not contain the requested key, an error is thrown:
Cache miss for key: usersThis strategy is useful for offline-first scenarios or when network access must not be used.
Network-Only
network-only always executes the underlying operation and does not use cached data.
Request
│
▼
Execute Operation
│
▼
Return ResultUsage:
const result = await layer.run({
key: "users",
cache: {
strategy: "network-only",
},
execute: () => fetchUsers(),
});The cache is ignored.
This strategy is useful when the application always requires fresh data.
Stale-While-Revalidate
stale-while-revalidate returns cached data immediately when available, while refreshing the data in the background.
Request
│
▼
Check Cache
│
├── Hit
│ │
│ ├──► Return Cached Data
│ │
│ └──► Revalidate in Background
│ │
│ ▼
│ Store Result
│
└── Miss
│
▼
Execute Operation
│
▼
Store Result
│
▼
Return DataUsage:
const result = await layer.run({
key: "users",
cache: {
strategy: "stale-while-revalidate",
},
execute: () => fetchUsers(),
});Behavior when cached data exists:
- Return cached data immediately.
- Execute the operation in the background.
- Store the fresh result in the cache.
- The background request does not affect the already returned response if it fails.
When cached data does not exist:
- Execute the operation normally.
- Store the result.
- Return the result.
This strategy is useful when fast responses are more important than waiting for fresh data.
Cache TTL
The execution context can optionally provide a TTL value for cache implementations that support expiration.
const result = await layer.run({
key: "users",
cache: {
strategy: "cache-first",
ttl: 60_000,
},
execute: () => fetchUsers(),
});The TTL value is expressed in milliseconds.
60_000 // 60 secondsThe cache store is responsible for deciding how TTL is handled.
Complete Example
import {
createLayer,
MemoryStore,
} from "@noj-tech/data-layer";
const layer = createLayer();
const cache = new MemoryStore();
layer.register("cache", cache);
const users = await layer.run({
key: "users",
cache: {
strategy: "cache-first",
ttl: 60_000,
},
execute: async () => {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
return response.json();
},
});The first request executes the network operation and stores the result.
Subsequent requests can return the cached result according to the selected strategy.
Framework Agnostic
@noj-tech/data-layer does not depend on Vue, React, Nuxt, Axios, Fetch, or any other specific technology.
You can use any HTTP client or data source you prefer.
Fetch
const api = {
async getUsers() {
const response = await fetch("/users");
return response.json();
},
};Axios
const api = {
async getUsers() {
const response = await axios.get("/users");
return response.data;
},
};The data layer manages the execution and caching architecture without controlling how the actual request is performed.
Extensibility
The architecture is based on modules and registries, allowing additional capabilities to be added without tightly coupling them to the core.
Possible modules include:
- Cache
- Storage
- API clients
- Persistence
- Logging
- Authentication
- Custom execution strategies
The core remains intentionally small while applications can add only the functionality they need.
Example
import { createLayer } from "@noj-tech/data-layer";
const layer = createLayer();
layer.register("cache", cache);
const result = await layer.run({
key: "users",
cache: {
strategy: "cache-first",
},
execute: () => fetchUsers(),
});Testing
The project uses Vitest for unit testing.
Run the test suite:
npm testOr:
npx vitestRun tests in watch mode:
npm run test:watchDevelopment
Clone the repository:
git clone https://github.com/Noj-Tech/data-layer.gitInstall dependencies:
npm installRun tests:
npm testBuild the package:
npm run buildDesign Goals
The main goals of @noj-tech/data-layer are:
- Keep the core lightweight.
- Remain framework-agnostic.
- Avoid coupling data management to a specific HTTP client.
- Make caching composable.
- Provide predictable execution strategies.
- Provide a modular and extensible architecture.
- Make the package reusable across different applications and environments.
@noj-tech Ecosystem
@noj-tech/data-layer is part of the @noj-tech package ecosystem.
Related packages can follow the same naming convention:
@noj-tech/data-layer
@noj-tech/...Each package should have a focused responsibility and remain independently usable whenever possible.
License
MIT
