@fiscozen/data
v0.2.3
Published
Design System Data Layer
Keywords
Readme
@fiscozen/data
Data Layer – Reactive Composable Architecture
Package users: installation, setup, API, examples, and merge helpers → Storybook > Documentation > Data Layer.
This README targets developers working on the package (architecture, code layout, development guide, testing).
What this package does: Structured data layer for Vue 3 apps (reactive CRUD, CSRF, deduplication, interceptors, merge helpers), built on @vueuse/core. Production-ready (v1.0).
Table of Contents
- Current Status
- Objective
- Architecture
- Package Exports
- Backend Conventions
- Development Guide
- Architectural Decisions
- Roadmap
- Appendix
1. Current Status
✅ Completed (v1.0)
Core Features:
- ✅ Complete CRUD operations (
useRetrieve,useList,useCreate,useUpdate,useDelete) - ✅ Reactive parameters (auto-refetch when filters/sort/page change)
- ✅ CSRF token auto-injection for mutations (POST/PUT/PATCH/DELETE)
- ✅ Request deduplication (configurable globally and per-action)
- ✅ Request and response interceptors (modify requests/responses, abort requests)
- ✅ Reactive body and headers in
useFzFetch(MaybeRefOrGetter; re-evaluated on eachexecute()) - ✅ Per-action trailing slash override (
trailingSlashin QueryActionOptions and MutationActionOptions; overrides setup for that action) - ✅ Debug logging (configurable via
setupFzFetcher({ debug: true })) - ✅ Call-with-defaults helpers for custom actions (
callListActionWithDefaults,callPaginatedListActionWithDefaults,callRetrieveActionWithDefaults,callCreateActionWithDefaults,callUpdateActionWithDefaults,callDeleteActionWithDefaults) - ✅ Filter semantics:
undefined= omit from request,null= send to server - ✅ TypeScript-first approach with complete type safety
- ✅ Subpath exports (
@fiscozen/data/rest)
Architecture:
- ✅ Layer separation (Component → Entity → Actions → HTTP)
- ✅ Built on
@vueuse/corefor robust foundations - ✅ Modular and extensible design
Documentation:
- ✅ Complete API reference
- ✅ Usage examples and patterns
- ✅ JSDoc on all public methods
🔮 Future Enhancements (v2.0)
- [ ] Response caching and cache invalidation
- [ ] Optimistic updates for mutations
- [ ] Retry logic with exponential backoff
- [ ] GraphQL adapter alongside REST
- [ ] Real-time updates via WebSockets integration
- [ ] Devtools integration
- [ ] Offline support with request queue
- [ ] Response pagination metadata parsing
2. Objective
The aim is to design an architecture in which each entity (for instance, User) has its own dedicated composable (useUsers) encapsulating all data-handling logic, built on top of shared lower-level layers.
Key benefits:
- Cleaner components, free from embedded API code
- Consistent structure across all entities
- Centralized handling of state, errors, and loading indicators
- Strong TypeScript typing
- Easier refactoring and testing
Target Usage:
- Primarily for external consumer applications (not just internal design system usage)
- Vue 3 only (no framework-agnostic requirement)
- Client-side rendering only (no SSR support required)
3. Architecture
Layer Structure
Vue component
↓ uses useUsers, etc.
Entity composable (useUsers)
↓ uses useActions
Generic CRUD API (useActions)
↓ uses useFzFetch
HTTP wrapper (useFzFetch)
↓ uses @vueuse/core createFetch
@vueuse/core createFetchLayers Explained
Entity Composable - Domain-specific composables (e.g.,
useUsers,useInvoices)- Created by developers in their applications
- Provides clean, typed interface for each entity
- Hides implementation complexity
Generic Actions -
useActions<T>()providing CRUD operations- TypeScript generics for type safety
- Reactive parameters support
- Smart query building (filters, sorting, pagination)
- Consistent API across all entities
HTTP Layer -
useFzFetchwith reactive URL, body, and headers- Built on
@vueuse/core'screateFetch - Reactive URLs (auto-refetch when computed URLs change)
- Reactive body and headers (
MaybeRefOrGetter; re-evaluated on eachexecute()) - Query parameter merging
- CSRF token auto-injection
- Request deduplication
- Built on
Foundation -
@vueuse/corefor robust state management
4. Package Exports
The package provides building blocks, not entity-specific composables:
// Package exports (from @fiscozen/data/rest or @fiscozen/data)
export {
useActions, // Generic CRUD factory
setupFzFetcher, // Setup configuration
useFzFetch, // Low-level HTTP wrapper
resetFzFetcher,
callListActionWithDefaults,
callPaginatedListActionWithDefaults,
callRetrieveActionWithDefaults,
callCreateActionWithDefaults,
callUpdateActionWithDefaults,
callDeleteActionWithDefaults,
}
export type * from './types'Developers create their own entity composables using these building blocks.
5. Backend Conventions
The package expects REST APIs with pagination (page/page_size), ordering (ordering=name,-created_at), filters as query params, URL versioning, and nested relations. Details and examples: Storybook > Documentation > Data Layer.
6. Development Guide
This section provides detailed information for developers working on the package internals, including architecture, code structure, and how to make fixes or add features.
6.1 Package Structure
packages/data/
├── src/
│ ├── rest/
│ │ ├── actions/ # CRUD operations layer
│ │ │ ├── create/ # Create action (POST)
│ │ │ ├── delete/ # Delete action (DELETE)
│ │ │ ├── list/ # List action (GET with filters/ordering/pagination)
│ │ │ ├── paginated-list/ # Paginated list action (GET with paginated response)
│ │ │ ├── retrieve/ # Retrieve action (GET by ID)
│ │ │ ├── update/ # Update action (PUT/PATCH)
│ │ │ ├── shared/ # Shared utilities for actions
│ │ │ │ ├── merge.ts # Merge helpers for custom actions
│ │ │ │ ├── normalize.ts
│ │ │ │ └── ...
│ │ │ └── index.ts # Main export (useActions factory)
│ │ │
│ │ └── http/ # HTTP layer (low-level)
│ │ ├── features/ # Feature modules
│ │ │ ├── deduplication/ # Request deduplication
│ │ │ └── interceptors/ # Request/response interceptors
│ │ ├── managers/ # State managers
│ │ │ ├── csrf/ # CSRF token management
│ │ │ └── deduplication/ # Deduplication manager
│ │ ├── setup/ # Global setup and state
│ │ ├── types/ # TypeScript type definitions
│ │ ├── utils/ # Utility functions
│ │ │ ├── csrf.ts # CSRF token injection
│ │ │ ├── error.ts # Error normalization
│ │ │ ├── options.ts # Options normalization
│ │ │ ├── response.ts # Response parsing
│ │ │ └── url.ts # URL building and query params
│ │ ├── wrappers/ # Wrapper pattern implementation
│ │ │ ├── adapters.ts # Wrapper adapters
│ │ │ ├── chain.ts # WrapperChain class
│ │ │ └── types.ts # Wrapper interfaces
│ │ ├── common.ts # Common constants
│ │ ├── config.ts # Configuration constants
│ │ └── index.ts # Main export (useFzFetch)
│ │
│ └── __tests__/ # Test files
│ ├── csrf.test.ts
│ ├── deduplication.test.ts
│ ├── interceptors.test.ts
│ ├── integration.test.ts
│ ├── list.test.ts
│ ├── merge.test.ts # Merge helpers
│ ├── normalize.test.ts
│ ├── paginated-list.test.ts
│ ├── setup.test.ts
│ └── url.test.ts
│
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md6.2 Architecture Overview
The package follows a layered architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────────┐
│ Component Layer (Consumer Application) │
│ - useUsers, useInvoices, etc. │
└────────────────────┬──────────────────────────────────┘
│
┌────────────────────▼──────────────────────────────────┐
│ Actions Layer (src/rest/actions/) │
│ - useActions<T>() factory │
│ - createRetrieveAction, createListAction, etc. │
│ - Handles: query building, reactive params, CRUD ops │
└────────────────────┬──────────────────────────────────┘
│
┌────────────────────▼──────────────────────────────────┐
│ HTTP Layer (src/rest/http/) │
│ - useFzFetch() wrapper │
│ - WrapperChain pattern │
│ - Features: CSRF, deduplication, interceptors │
└────────────────────┬──────────────────────────────────┘
│
┌────────────────────▼──────────────────────────────────┐
│ Foundation (@vueuse/core) │
│ - createFetch │
│ - Reactive state management │
└───────────────────────────────────────────────────────┘6.3 Core Components Explained
6.3.1 HTTP Layer (src/rest/http/)
Entry Point: index.ts
- Exports
useFzFetch- main HTTP wrapper function - Handles 3 overload cases (basePath only, basePath + params, basePath + params + options)
- Uses
createFetchResulthelper to apply wrappers viaWrapperChain
Wrapper Pattern (wrappers/)
chain.ts:WrapperChainclass that applies wrappers sequentiallyadapters.ts: Adapter implementations for each wrapper (request interceptor, response interceptor, deduplication)types.ts:Wrapperinterface andWrapperContexttype
How Wrappers Work:
// 1. Base fetch result is created
const baseFetchResult = state.fzFetcher<T>(url, requestInit).json()
// 2. Wrapper chain is created with default wrappers
const chain = new WrapperChain()
chain.add(requestInterceptorWrapper) // Applied first
chain.add(responseInterceptorWrapper) // Applied second
chain.add(deduplicationWrapper) // Applied last
// 3. Wrappers are applied sequentially
return chain.apply(baseFetchResult, context)Why This Pattern?
- Easy to add/remove wrappers without modifying core code
- Clear execution order
- Testable in isolation
- Composable functionality
6.3.2 Features (features/)
Request Interceptor (features/interceptors/request.ts)
- Wraps
execute()method to intercept requests before fetch - Can modify
requestInitor abort request (returnnull) - If
requestInitis modified, creates new fetch call with modified config - Uses reactive
watchto sync state from modified fetch to original result
Response Interceptor (features/interceptors/response.ts)
- Wraps
execute()method to intercept responses after fetch - Can modify response body (requires re-parsing)
- Uses
parseResponseBodyutility for consistent parsing
Deduplication (features/deduplication/)
wrapper.ts: Wrapsexecute()to check for pending identical requestsmanagers/deduplication/index.ts:DeduplicationManagerclass- Generates unique keys:
method:normalizedUrl:normalizedBody - Tracks pending requests in
Map - Uses
watchEffectto sync state reactively - Cleans up automatically when request completes
- Generates unique keys:
6.3.3 Managers (managers/)
CSRF Manager (managers/csrf/index.ts)
- Reads CSRF token from cookie (handles values with
=) - Injects token into headers for mutation methods (POST/PUT/PATCH/DELETE)
- Uses
CsrfManagerclass withinjectToken()method
Deduplication Manager (managers/deduplication/index.ts)
- Generates deduplication keys
- Normalizes URLs (removes trailing slashes, sorts query params)
- Normalizes bodies (sorts JSON keys, creates identifier for non-JSON)
- Tracks pending requests and watches for completion
6.3.4 Setup & State (setup/)
state.ts: Global singleton state
fzFetcher:@vueuse/corefetch instanceglobalBaseUrl: Base URL for API requestsglobalCsrfOptions: CSRF configurationcsrfManager: CSRF manager instancededuplicationManager: Deduplication manager instanceglobalRequestInterceptor: Request interceptor functionglobalResponseInterceptor: Response interceptor functionglobalDebug: Debug logging flag
index.ts: Setup function
setupFzFetcher(): Initializes global stateresetFzFetcher(): Resets state (useful for testing)
6.3.5 Utils (utils/)
error.ts: normalizeError(error: unknown): Error
- Converts any error-like value to
Errorinstance - Handles strings, objects with
messageproperty, etc.
response.ts: parseResponseBody<T>(response: Response): Promise<T>
- Parses response based on Content-Type header
- Tries JSON first, falls back to text
- Handles all content types consistently
url.ts: getUrlWithQueryParams(basePath, queryParams)
- Builds URL with query parameters
- Handles reactive
queryParams - Merges existing query params from URL
csrf.ts: injectCsrfToken(method, headers)
- Injects CSRF token for mutation methods
- Returns headers with CSRF token added
options.ts: normalizeUseFzFetchOptions(options)
- Normalizes
UseFzFetchOptionsfor@vueuse/core - Handles default values and type conversions
6.3.6 Actions Layer (actions/)
Factory Pattern: useActions<T>(basePath)
- Returns object with 5 action creators:
useRetrieve: Get single entity by IDuseList: List entities with filters/ordering/paginationusePaginatedList: List entities with paginated response and metadatauseCreate: Create new entity (POST)useUpdate: Update entity (PUT/PATCH)useDelete: Delete entity (DELETE)
Each Action Creator:
- Returns composable with
data,error,isLoading,execute - Handles reactive parameters (auto-refetch)
- Builds URLs and query strings
- Uses
useFzFetchinternally
Shared Utilities (shared/):
types.ts: Common types for all actionsnormalize.ts: Normalizes action options (includingtrailingSlashpass-through to UseFzFetchOptions),isParamsObject(params vs options discrimination),mutationOptionsToFetchOptions(MutationActionOptions → UseFzFetchOptions for create/update/delete)error-handling.ts: Centralized error handlingmerge.ts: Merge helpers for custom actions (default + view → merge → action)
6.3.7 Call-with-defaults helpers (shared/merge.ts)
Call-with-defaults helpers let package consumers call an action with default args merged with additional args from the view. One helper per action type: list, paginated list, retrieve, create, update, delete. User-facing usage and examples: Storybook > Documentation > Data Layer.
Location: src/rest/actions/shared/merge.ts. Exported from rest/actions/index.ts and main index.ts.
Public API: callListActionWithDefaults, callPaginatedListActionWithDefaults, callRetrieveActionWithDefaults, callCreateActionWithDefaults, callUpdateActionWithDefaults, callDeleteActionWithDefaults. Input types: MergeListActionArgsInput (defaultParams, defaultOptions, additionalParamsOrOptions, additionalOptions), MergeRetrieveActionArgsInput, MergeMutationActionArgsInput. Internal (used by the helpers, not re-exported): mergeListActionArgs, mergeRetrieveActionArgs, mergeMutationActionArgs.
List discrimination: When the view passes a single argument, we distinguish params vs options via isParamsObject (presence of filters, ordering, or pagination). When additionalOptions is present, additionalParamsOrOptions is treated as params.
Merge semantics: Filters and pagination: object spread (view wins on same key). Ordering: use view's if view provided an ordering key, else default. Options: object spread (view wins). Retrieve: overridePk overrides defaultPk when provided; options merged. Mutations: options merged.
Filter semantics (query layer): undefined = omit from request (remove default filter); null = send to server. Implemented in normalize.ts (filters) and http/utils/url.ts (query params).
Tests: src/__tests__/merge.test.ts (discrimination, merge semantics, ref preservation, toValue on defaults).
6.4 How to Fix a Bug
Step 1: Identify the Problem
- Reproduce the bug: Create a minimal test case
- Locate the code: Use code search to find relevant files
- Understand the flow: Trace execution path from entry point
Example: Fixing a bug in CSRF token injection
// 1. Bug: CSRF token not injected for PATCH requests
// 2. Search for CSRF-related code
grep -r "csrf" src/rest/http/
// 3. Check CSRF manager
// File: src/rest/http/managers/csrf/index.ts
// Method: injectToken() - checks MUTATION_METHODS
// 4. Check MUTATION_METHODS constant
// File: src/rest/http/common.ts
// Verify PATCH is included: ["POST", "PUT", "PATCH", "DELETE"]Step 2: Write a Failing Test
// src/__tests__/csrf.test.ts
it('should inject CSRF token for PATCH requests', () => {
document.cookie = 'csrf_token=test-token'
setupFzFetcher({
baseUrl: 'https://api.example.com',
csrf: { enabled: true, cookieName: 'csrf_token' }
})
const { execute } = useFzFetch('/test', { method: 'PATCH' })
// Mock fetch to verify headers
let interceptedHeaders: HeadersInit | undefined
global.fetch = vi.fn((_url, init) => {
interceptedHeaders = init?.headers
return Promise.resolve(new Response())
}) as typeof fetch
await execute()
// Verify CSRF token was injected
expect(interceptedHeaders).toHaveProperty('X-CSRF-Token', 'test-token')
})Step 3: Fix the Code
// Fix in src/rest/http/managers/csrf/index.ts
// Ensure PATCH is in MUTATION_METHODS (already correct)
// Check injectToken logic (already correct)
// Verify wrapper is applied (check createDefaultWrapperChain)Step 4: Verify the Fix
# Run tests
pnpm --filter @fiscozen/data test:unit
# Run linting
pnpm --filter @fiscozen/data lint
# Test manually in browser/devtoolsStep 5: Update Documentation
- Update README if API changed
- Update JSDoc comments if behavior changed
- Add examples if new use case
6.5 How to Add a Feature
Example: Adding Request Timeout Feature
Step 1: Design the API
// Setup options
setupFzFetcher({
baseUrl: 'https://api.example.com',
timeout: 5000 // 5 seconds (global)
})
// Per-action override
useListUsers({ filters: {} }, { timeout: 10000 })Step 2: Create Feature Module
// src/rest/http/features/timeout/types.ts
export interface TimeoutOptions {
timeout?: number | null // null = no timeout
}
// src/rest/http/features/timeout/wrapper.ts
export const wrapWithTimeout = <T>(
fetchResult: UseFzFetchReturn<T>,
timeout: number | null | undefined,
): UseFzFetchReturn<T> => {
if (!timeout) return fetchResult
const originalExecute = fetchResult.execute
fetchResult.execute = async (throwOnFailed?: boolean) => {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
// Add signal to requestInit
// Execute with timeout
await originalExecute(throwOnFailed)
} finally {
clearTimeout(timeoutId)
}
}
return fetchResult
}Step 3: Create Wrapper Adapter
// src/rest/http/wrappers/adapters.ts
export const timeoutWrapper: Wrapper = {
wrap<T>(
fetchResult: UseFzFetchReturn<T> & PromiseLike<UseFzFetchReturn<T>>,
context: WrapperContext,
): UseFzFetchReturn<T> & PromiseLike<UseFzFetchReturn<T>> {
const timeout = context.useFetchOptions?.timeout ?? state.globalTimeout
return wrapWithTimeout(fetchResult, timeout)
},
}Step 4: Add to Wrapper Chain
// src/rest/http/index.ts
const createDefaultWrapperChain = (): WrapperChain => {
const chain = new WrapperChain()
chain.add(requestInterceptorWrapper)
chain.add(responseInterceptorWrapper)
chain.add(timeoutWrapper) // ← Add here
chain.add(deduplicationWrapper)
return chain
}Step 5: Update Types
// src/rest/http/types/core.ts
export interface UseFzFetchOptions extends UseFetchOptions {
timeout?: number | null
}
// src/rest/http/types/setup.ts
export interface SetupFzFetcherOptions {
timeout?: number | null
}Step 6: Update State
// src/rest/http/setup/state.ts
export const state = {
globalTimeout: null as number | null,
// ... other state
}Step 7: Write Tests
// src/__tests__/timeout.test.ts
describe('Timeout Feature', () => {
it('should abort request after timeout', async () => {
setupFzFetcher({
baseUrl: 'https://api.example.com',
timeout: 100
})
global.fetch = vi.fn(() =>
new Promise(resolve => setTimeout(() => resolve(new Response()), 1000))
) as any
const { execute, error } = useFzFetch('/test', { immediate: false })
await execute()
expect(error.value).toBeInstanceOf(Error)
expect(error.value?.name).toBe('AbortError')
})
})Step 8: Update Documentation
- Add to README Configuration section
- Add examples
- Update API reference
6.6 Key Patterns & Conventions
6.6.1 Wrapper Pattern
When to Use:
- Adding cross-cutting concerns (logging, timeout, retry)
- Modifying request/response behavior
- Adding functionality that wraps the entire fetch flow
How to Implement:
- Create wrapper function in
features/[feature-name]/wrapper.ts:
export const wrapWithFeature = <T>(
fetchResult: UseFzFetchReturn<T> & PromiseLike<UseFzFetchReturn<T>>,
// ... feature-specific params
): UseFzFetchReturn<T> & PromiseLike<UseFzFetchReturn<T>> => {
const originalExecute = fetchResult.execute
fetchResult.execute = async (throwOnFailed?: boolean) => {
// Feature logic here
return originalExecute(throwOnFailed)
}
return fetchResult
}- Create adapter in
wrappers/adapters.ts:
export const featureWrapper: Wrapper = {
wrap<T>(fetchResult, context) {
return wrapWithFeature(fetchResult, /* params from context */)
},
}- Add to wrapper chain in
index.ts:
chain.add(featureWrapper)6.6.2 Error Handling Pattern
Always use normalizeError:
import { normalizeError } from '../utils/error'
try {
// code that might throw
} catch (error: unknown) {
const normalizedError = normalizeError(error)
fetchResult.error.value = normalizedError
if (throwOnFailed) {
throw normalizedError
}
}Why: Ensures consistent error types across the codebase.
6.6.3 State Synchronization Pattern
When syncing state between fetch results, use watchEffect:
import { watchEffect, nextTick } from 'vue'
const unwatchSync = watchEffect(() => {
// Sync all reactive properties
targetResult.data.value = sourceResult.data.value
targetResult.error.value = sourceResult.error.value
// ...
// Cleanup when done
if (!sourceResult.isFetching.value) {
nextTick(() => {
unwatchSync()
})
}
})Why: watchEffect automatically tracks all accessed reactive properties, more efficient than explicit watch arrays.
6.6.4 URL Normalization Pattern
Always normalize URLs for deduplication keys:
// Use DeduplicationManager.normalizeUrl() internally
// Or create helper if needed elsewhere
const normalizedUrl = normalizeUrl(url) // Removes trailing slashes, sorts query paramsWhy: Ensures identical requests generate identical keys.
6.6.5 Testing Patterns
Unit Tests:
- Test one feature/function at a time
- Mock dependencies (
global.fetch,state) - Use
resetFzFetcher()inbeforeEach
Integration Tests:
- Test complete flow (setup → request → interceptor → response)
- Verify state synchronization
- Test error handling end-to-end
Test Structure:
describe('FeatureName', () => {
beforeEach(() => {
resetFzFetcher()
vi.clearAllMocks()
})
describe('SpecificBehavior', () => {
it('should do something', async () => {
// Arrange
setupFzFetcher({ baseUrl: 'https://api.test.com' })
global.fetch = vi.fn(() => Promise.resolve(new Response()))
// Act
const { execute } = useFzFetch('/test')
await execute()
// Assert
expect(global.fetch).toHaveBeenCalledWith(...)
})
})
})6.7 Common Tasks
Adding a New Action Type
- Create action directory:
src/rest/actions/[action-name]/ - Create
index.tswith action creator function - Create
types.tswith types - Export from
src/rest/actions/index.ts - Add to
useActionsfactory return type
Modifying Request Behavior
- Check if existing wrapper can handle it
- If not, create new wrapper in
features/[feature-name]/ - Add adapter to
wrappers/adapters.ts - Add to wrapper chain in
index.ts
Adding Global Configuration
- Add to
SetupFzFetcherOptionsintypes/setup.ts - Store in
state.ts - Use in setup function (
setup/index.ts) - Document in README Configuration section
Debugging Tips
Enable Debug Logging:
setupFzFetcher({ baseUrl: '...', debug: true })
// Logs all requests, responses, and state changesCheck State:
import { state } from '@fiscozen/data/rest/http/setup/state'
console.log(state) // Inspect global stateTrace Execution:
- Add
console.login wrapperexecutemethods - Check browser Network tab for actual requests
- Use Vue DevTools to inspect reactive state
6.8 Code Quality Standards
TypeScript:
- No
anytypes in public API - Use
unknownfor error types, then normalize - Export types from dedicated
types.tsfiles
Vue 3:
- Use
<script setup lang="ts">for components - Use
computed,ref,watchfrom Vue - Prefer
watchEffectfor automatic dependency tracking
Error Handling:
- Always use
normalizeError()utility - Set
error.valuebefore throwing (ifthrowOnFailed) - Clean up watches/resources in finally blocks
Performance:
- Use
watchEffectinstead of multiplewatchcalls - Clean up watches with
nextTickfor proper timing - Memoize expensive computations
Documentation:
- JSDoc on all public functions
- Explain "why" not "what" in comments
- Include
@examplefor complex functions - Document limitations and edge cases
6.9 File Naming Conventions
- Test files:
__tests__/[feature].test.ts - Type files:
types.tsin same directory ortypes/[name].ts - Utility files:
utils/[name].ts - Feature modules:
features/[feature-name]/[file].ts - Manager classes:
managers/[manager-name]/index.ts
6.10 Testing Checklist
Before submitting a fix or feature:
- [ ] Unit tests written and passing
- [ ] Integration tests added (if applicable)
- [ ] Linting passes (
pnpm lint) - [ ] Type checking passes (
pnpm type-check) - [ ] README updated (if API changed)
- [ ] JSDoc comments added/updated
- [ ] Examples added (if new feature)
- [ ] Edge cases handled
- [ ] Error cases tested
- [ ] Cleanup verified (watches, timers, etc.)
7. Architectural Decisions
Singleton Pattern
The package uses a singleton pattern for global fetcher state:
// Setup once at app initialization
setupFzFetcher({ baseUrl: '...' })
// Use anywhere in the app
const { useList } = useActions<User>('users')Rationale:
- Simple and ergonomic API
- No SSR requirement (CSR only)
- Suitable for single-tenant applications
Future Consideration:
- If multi-tenancy or SSR support is needed, consider Provide/Inject pattern
Export Structure
Two import patterns are supported:
// ✅ Recommended: Subpath import
import { useActions, setupFzFetcher } from '@fiscozen/data/rest'
// ✅ Alternative: Namespace import
import { rest } from '@fiscozen/data'
const { useActions, setupFzFetcher } = restRationale:
- Subpath imports allow future extensions (
/graphql,/websocket) - Namespace imports maintain backward compatibility
Type Discrimination
Multiple function overloads are used for ergonomic API:
// All valid:
useList()
useList({ filters: { active: true } })
useList({ filters: { active: true } }, { onMount: false })Rationale:
- More ergonomic than discriminated unions
- TypeScript handles overload resolution automatically
Reactive Parameters
All parameters support MaybeRefOrGetter<T>:
const filters = ref({ active: true })
useList({ filters }) // ✅ Reactive
useList({ filters: { active: true } }) // ✅ Static also worksRationale:
- Enables dynamic filtering/pagination
- Backward compatible with static values
CSRF Protection
CSRF tokens are automatically injected for mutation methods:
- Enabled methods: POST, PUT, PATCH, DELETE
- Disabled methods: GET, HEAD, OPTIONS
- Workflow: Backend sets cookie → Package reads cookie → Package injects header
Rationale:
- Required for session cookie authentication
- Automatic injection reduces developer burden
Request Deduplication
Configurable globally and per-action:
- Key: URL + query string + payload + method
- Behavior: Identical simultaneous requests share the same promise
- Benefit: Prevents duplicate API calls
Rationale:
- Common pattern in modern data fetching libraries (SWR, TanStack Query)
- Reduces server load and improves performance
8. Roadmap
v1.0 (Current) ✅ COMPLETE
All features completed:
Core Features:
- ✅ Complete CRUD operations (
useRetrieve,useList,useCreate,useUpdate,useDelete) - ✅ Reactive parameters (auto-refetch when filters/sort/page change)
- ✅ CSRF token auto-injection for mutations (POST/PUT/PATCH/DELETE)
- ✅ Request deduplication (configurable globally and per-action)
- ✅ Request and response interceptors (modify requests/responses, abort requests)
- ✅ Debug logging (configurable via
setupFzFetcher({ debug: true }))
Architecture & Quality:
- ✅ TypeScript-first approach with complete type safety
- ✅ Subpath exports (
@fiscozen/data/rest) - ✅ Layer separation (Component → Entity → Actions → HTTP)
- ✅ Built on
@vueuse/corefor robust foundations - ✅ Modular and extensible design
- ✅ Input validation for all public APIs
- ✅ Centralized constants (no magic strings/numbers)
- ✅ Consistent error handling patterns
Documentation:
- ✅ Complete API reference
- ✅ Usage examples and patterns
- ✅ JSDoc on all public methods
- ✅ Comprehensive README
Package Status:
- ✅ Production-ready
- ✅ All critical bugs fixed
- ✅ Type safety complete (no
anytypes) - ✅ Test suite organized by feature
- ✅ Build optimized (21.24 kB, gzip: 5.65 kB)
v2.0 (Future Enhancements)
Caching & Performance:
- [ ] In-memory response caching (configurable globally and per-action)
- [ ] Cache invalidation after mutations
- [ ] Background refetching (stale-while-revalidate)
- [ ] Optimistic updates for mutations
Error Handling & Resilience:
- [ ] Retry logic with exponential backoff (configurable globally and per-action)
- [ ] Automatic retry for network errors (5xx) - not for 4xx
- [ ] Request timeouts (configurable globally and per-action)
- Abort requests that exceed configured timeout duration
- Support for infinite timeout (
null) and per-action overrides - Proper cleanup and error handling
- [ ] Offline support with request queue
Developer Experience:
- [ ] Devtools integration for debugging
- [ ] Mock/test utilities (for unit/integration tests, exported from
@fiscozen/data/testing)- Export strategy: Separate path
@fiscozen/data/testing(tree-shakeable) - Use cases: Unit tests for composables, integration tests for components
- Example:
mockFzFetch('/users', { data: [...] })for testing
- Export strategy: Separate path
- [ ] Response pagination metadata parsing
- [ ] File upload support (
useUploadFilecomposable)- Dedicated composable for file uploads with progress tracking
- Proper
FormDatahandling andmultipart/form-datacontent type - Integration with entity composables (e.g.,
uploadDocumentinuseUsers) - Example:
const { execute: uploadFile, progress, isLoading } = useUploadFile('/documents')
Protocol Support:
- [ ] GraphQL adapter alongside REST
- [ ] WebSockets integration for real-time updates
Architecture:
- [ ] Adapter pattern for swappable HTTP clients (Axios/Fetch)
- Allow swapping underlying HTTP client without affecting higher layers
- Plug-n-play mechanism for different request implementations
- [ ] Multi-backend configuration support
- Support for multiple backend configurations with different conventions
- Per-backend pagination, sorting, and filtering conventions
Testing & Quality:
- [ ] Enhanced test coverage (>90%)
- [ ] Edge cases handling (URL with hash, array empty values, etc.)
- [ ] Cookie parsing improvements (handle values containing
=)
File Upload Support:
- [ ]
useUploadFilecomposable for file uploads- Progress tracking (
progressref with upload percentage) - Proper
FormDatahandling - Support for multiple files
- Integration with entity composables
- Example usage in entity composable:
export const useUsers = () => { const { useUploadFile } = useActions<User>('users') return { uploadDocument: useUploadFile('/documents'), // ... other actions } }
- Progress tracking (
Development History & Decisions
Key Architectural Decisions
Singleton Pattern:
- Chosen for simplicity and ergonomic API
- Suitable for CSR-only applications (no SSR requirement)
- Future consideration: Provide/Inject pattern if multi-tenancy or SSR support is needed
Export Structure:
- Subpath imports (
@fiscozen/data/rest) recommended for future extensibility - Namespace imports (
import { rest } from '@fiscozen/data') maintained for backward compatibility - Allows future extensions (
/graphql,/websocket)
Type Discrimination:
- Multiple function overloads used instead of discriminated unions
- More ergonomic API while maintaining type safety
- TypeScript handles overload resolution automatically
Reactive Parameters:
- All parameters support
MaybeRefOrGetter<T>for maximum flexibility - Enables dynamic filtering/pagination while maintaining backward compatibility
- Auto-refetch when reactive dependencies change
CSRF Protection:
- Automatic injection for mutation methods (POST/PUT/PATCH/DELETE)
- Workflow: Backend sets cookie → Package reads cookie → Package injects header
- Reduces developer burden while maintaining security
Request Deduplication:
- Key includes: URL + query string + payload + method
- Identical simultaneous requests share the same promise
- Prevents duplicate API calls when components mount simultaneously
- Common pattern in modern data fetching libraries (SWR, TanStack Query)
Backend Conventions Supported
This package is designed to work with REST APIs following these conventions:
- Pagination:
page/page_sizequery parameters - Ordering:
ordering=name,-created_at(comma-separated fields, descending fields prefixed with '-') - Filtering: Direct query parameters (e.g.,
?by_city=rome&by_status=active) - API Versioning: URL-based (e.g.,
/v1/users,/v2/users) - Nested Relations: Nested routes (e.g.,
GET /users/1/invoices) - Authentication: Session cookies (CSRF protection supported)
- Error Format: No standard format enforced - errors exposed via
errorref for consumer to handle
Package Statistics
- Bundle Size: 21.24 kB (gzip: 5.65 kB)
- Lines of Code: ~1800 LOC
- Test Coverage: Organized by feature (setup, deduplication, interceptors)
- Type Safety: Complete (no
anytypes in public API) - Linting: ESLint with
@fiscozen/eslint-config; runpnpm lint(0 errors) - Build: Optimized and production-ready
9. Appendix
Glossary
- Entity composable: Domain-specific composable (e.g.,
useUsers) - useActions: Generic CRUD layer providing standardized operations
- useFzFetch: HTTP wrapper with reactive URL support
- MaybeRefOrGetter: Vue 3 type for reactive or static values
- Reactive parameters: Values that automatically trigger re-fetches when changed
Key Benefits
- 🔄 Reactive by design - Parameters auto-trigger fetches
- 📝 TypeScript-first - Complete type safety across all layers
- 🧩 Modular architecture - Clear separation of concerns
- ⚡ Performance-focused - Built on proven
@vueuse/corefoundation - 🎯 Developer experience - Consistent, predictable API patterns
- 🔒 Security - CSRF protection built-in
- 🚀 Performance - Request deduplication prevents duplicate calls
License
MIT
Contributing
This package is part of the Fiscozen Design System. For contributions, please follow the design system contribution guidelines.
