@noj-tech/data-layer
@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
- Cache TTL support
- Built-in IndexedDB, LocalStorage, and Memory stores
- 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-layer
Basic 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.
Default Export
The package also provides a ready-to-use default layer instance.
import layer from "@noj-tech/data-layer";
const result = await layer.run({
execute: async () => {
return fetchUsers();
},
});
Use createLayer() when you need your own layer instance or custom configuration.
TypeScript
@noj-tech/data-layer is TypeScript-first and exposes its public types from the package root.
import {
createLayer,
type LayerOptions,
type LayerContext,
type RunContext,
type RuntimeAPI,
type LayerModule,
type CacheOptions,
type CacheStore,
type CacheStrategy,
} from "@noj-tech/data-layer";
This allows TypeScript applications to use the package without importing internal files.
For example:
const strategy: CacheStrategy = "cache-first";
const options: LayerOptions = {
// ...
};
Custom modules can also use the public LayerModule type:
import type { LayerModule } from "@noj-tech/data-layer";
const myModule: LayerModule = {
name: "my-module",
install(context) {
// ...
},
};
Only types that are intended to be part of the public API are exported from the package root.
Internal implementation details such as the runner and registry are not part of the public API.
Architecture
The package provides a small runtime for managing data-related operations.
Application
│
▼
┌─────────────────────┐
│ Data Layer │
├─────────────────────┤
│ Runner │
│ Registry │
│ Modules │
│ Cache │
└─────────────────────┘
│
├── API / HTTP
├── Cache
├── Storage
└── Custom Modules
The core does not depend on a specific framework, HTTP client, or storage implementation.
Cache
Caching is a first-class capability of the data layer.
The package provides a cache module and multiple cache stores.
The default cache store is IndexedDB.
Available stores include:
IndexedDBStoreLocalStorageStoreMemoryStore
A cache can be accessed through the layer:
const cache = layer.get("cache");
The cache service provides:
await cache.set("users", users);
const users = await cache.get<User[]>("users");
await cache.remove("users");
await cache.clear();
Cache Stores
IndexedDBStore
IndexedDBStore provides persistent browser storage using IndexedDB.
It is the default storage used by the data layer.
import { IndexedDBStore } from "@noj-tech/data-layer";
const store = new IndexedDBStore();
LocalStorageStore
LocalStorageStore uses the browser's Local Storage API.
import { LocalStorageStore } from "@noj-tech/data-layer";
const store = new LocalStorageStore();
MemoryStore
MemoryStore keeps cached data in memory.
import { MemoryStore } from "@noj-tech/data-layer";
const store = new MemoryStore();
Memory storage is useful for temporary data and testing.
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 Data
Usage:
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 Error
Usage:
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 Error
Usage:
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: users
This 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 Result
Usage:
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 Data
Usage:
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.
- A background request failure does not affect the already returned response.
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
Cache entries can optionally have a TTL.
TTL values are expressed in milliseconds.
const result = await layer.run({
key: "users",
cache: {
strategy: "cache-first",
ttl: 60_000,
},
execute: () => fetchUsers(),
});
60_000 // 60 seconds
The TTL is handled by the data layer's cache manager.
When an entry expires, it is treated as a cache miss and removed from the cache when accessed.
For example:
await cache.set(
"access_token",
token,
7 * 24 * 60 * 60 * 1000,
);
An entry without a TTL does not expire automatically:
await cache.set("refresh_token", refreshToken);
It remains available until it is explicitly removed or the underlying storage is cleared.
Custom Cache Stores
The cache system is based on the CacheStore interface.
import type { CacheStore } from "@noj-tech/data-layer";
const customStore: CacheStore = {
async has(key) {
// ...
},
async get(key) {
// ...
},
async set(key, value) {
// ...
},
async remove(key) {
// ...
},
async clear() {
// ...
},
};
This allows applications to implement their own storage mechanism without changing the runner or cache architecture.
Custom Modules
The package uses a modular architecture.
Custom modules can implement the public LayerModule interface:
import type { LayerModule } from "@noj-tech/data-layer";
const myModule: LayerModule = {
name: "my-module",
install(context) {
// Register custom functionality
},
};
Modules can be used to extend the data layer without coupling additional functionality to the core runtime.
Possible modules include:
- Cache
- Storage
- API clients
- Persistence
- Logging
- Authentication
- Custom functionality
Complete Example
import {
createLayer,
type CacheStrategy,
} from "@noj-tech/data-layer";
const layer = createLayer();
const strategy: CacheStrategy = "cache-first";
const users = await layer.run({
key: "users",
cache: {
strategy,
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 underlying 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.
The core remains intentionally small while applications can add only the functionality they need.
Testing
The project uses Vitest for unit testing.
Run the test suite:
npm test
Or:
npx vitest
Run tests in watch mode:
npm run test:watch
Development
Clone the repository:
git clone https://github.com/Noj-Tech/data-layer.git
Install dependencies:
npm install
Run tests:
npm test
Build the package:
npm run build
Design 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.
- Support multiple storage implementations.
- Provide a TypeScript-friendly public API.
- 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
Related Packages
Leverage local storage to improve perfomance by caching data and expire cache
Type-safe Fetch HTTP client with retries, interceptors, validation, and multi-ho...
A nuxt.js module that implements a universal api layer, same-way compatible betw...
Version History
| Version | Released | License |
|---|---|---|
| 1.0.3 | MIT | |
| 1.0.2 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT |