npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fiscozen/data

v0.2.3

Published

Design System Data Layer

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

  1. Current Status
  2. Objective
  3. Architecture
  4. Package Exports
  5. Backend Conventions
  6. Development Guide
  7. Architectural Decisions
  8. Roadmap
  9. 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 each execute())
  • ✅ Per-action trailing slash override (trailingSlash in 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/core for 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 createFetch

Layers Explained

  1. 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
  2. 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
  3. HTTP Layer - useFzFetch with reactive URL, body, and headers

    • Built on @vueuse/core's createFetch
    • Reactive URLs (auto-refetch when computed URLs change)
    • Reactive body and headers (MaybeRefOrGetter; re-evaluated on each execute())
    • Query parameter merging
    • CSRF token auto-injection
    • Request deduplication
  4. Foundation - @vueuse/core for 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.md

6.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 createFetchResult helper to apply wrappers via WrapperChain

Wrapper Pattern (wrappers/)

  • chain.ts: WrapperChain class that applies wrappers sequentially
  • adapters.ts: Adapter implementations for each wrapper (request interceptor, response interceptor, deduplication)
  • types.ts: Wrapper interface and WrapperContext type

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 requestInit or abort request (return null)
  • If requestInit is modified, creates new fetch call with modified config
  • Uses reactive watch to 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 parseResponseBody utility for consistent parsing

Deduplication (features/deduplication/)

  • wrapper.ts: Wraps execute() to check for pending identical requests
  • managers/deduplication/index.ts: DeduplicationManager class
    • Generates unique keys: method:normalizedUrl:normalizedBody
    • Tracks pending requests in Map
    • Uses watchEffect to sync state reactively
    • Cleans up automatically when request completes

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 CsrfManager class with injectToken() 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/core fetch instance
  • globalBaseUrl: Base URL for API requests
  • globalCsrfOptions: CSRF configuration
  • csrfManager: CSRF manager instance
  • deduplicationManager: Deduplication manager instance
  • globalRequestInterceptor: Request interceptor function
  • globalResponseInterceptor: Response interceptor function
  • globalDebug: Debug logging flag

index.ts: Setup function

  • setupFzFetcher(): Initializes global state
  • resetFzFetcher(): Resets state (useful for testing)

6.3.5 Utils (utils/)

error.ts: normalizeError(error: unknown): Error

  • Converts any error-like value to Error instance
  • Handles strings, objects with message property, 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 UseFzFetchOptions for @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 ID
    • useList: List entities with filters/ordering/pagination
    • usePaginatedList: List entities with paginated response and metadata
    • useCreate: 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 useFzFetch internally

Shared Utilities (shared/):

  • types.ts: Common types for all actions
  • normalize.ts: Normalizes action options (including trailingSlash pass-through to UseFzFetchOptions), isParamsObject (params vs options discrimination), mutationOptionsToFetchOptions (MutationActionOptions → UseFzFetchOptions for create/update/delete)
  • error-handling.ts: Centralized error handling
  • merge.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

  1. Reproduce the bug: Create a minimal test case
  2. Locate the code: Use code search to find relevant files
  3. 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/devtools

Step 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:

  1. 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
}
  1. Create adapter in wrappers/adapters.ts:
export const featureWrapper: Wrapper = {
  wrap<T>(fetchResult, context) {
    return wrapWithFeature(fetchResult, /* params from context */)
  },
}
  1. 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 params

Why: 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() in beforeEach

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

  1. Create action directory: src/rest/actions/[action-name]/
  2. Create index.ts with action creator function
  3. Create types.ts with types
  4. Export from src/rest/actions/index.ts
  5. Add to useActions factory return type

Modifying Request Behavior

  1. Check if existing wrapper can handle it
  2. If not, create new wrapper in features/[feature-name]/
  3. Add adapter to wrappers/adapters.ts
  4. Add to wrapper chain in index.ts

Adding Global Configuration

  1. Add to SetupFzFetcherOptions in types/setup.ts
  2. Store in state.ts
  3. Use in setup function (setup/index.ts)
  4. Document in README Configuration section

Debugging Tips

Enable Debug Logging:

setupFzFetcher({ baseUrl: '...', debug: true })
// Logs all requests, responses, and state changes

Check State:

import { state } from '@fiscozen/data/rest/http/setup/state'
console.log(state) // Inspect global state

Trace Execution:

  • Add console.log in wrapper execute methods
  • Check browser Network tab for actual requests
  • Use Vue DevTools to inspect reactive state

6.8 Code Quality Standards

TypeScript:

  • No any types in public API
  • Use unknown for error types, then normalize
  • Export types from dedicated types.ts files

Vue 3:

  • Use <script setup lang="ts"> for components
  • Use computed, ref, watch from Vue
  • Prefer watchEffect for automatic dependency tracking

Error Handling:

  • Always use normalizeError() utility
  • Set error.value before throwing (if throwOnFailed)
  • Clean up watches/resources in finally blocks

Performance:

  • Use watchEffect instead of multiple watch calls
  • Clean up watches with nextTick for proper timing
  • Memoize expensive computations

Documentation:

  • JSDoc on all public functions
  • Explain "why" not "what" in comments
  • Include @example for complex functions
  • Document limitations and edge cases

6.9 File Naming Conventions

  • Test files: __tests__/[feature].test.ts
  • Type files: types.ts in same directory or types/[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 } = rest

Rationale:

  • 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 works

Rationale:

  • 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/core for 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 any types)
  • ✅ 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
  • [ ] Response pagination metadata parsing
  • [ ] File upload support (useUploadFile composable)
    • Dedicated composable for file uploads with progress tracking
    • Proper FormData handling and multipart/form-data content type
    • Integration with entity composables (e.g., uploadDocument in useUsers)
    • 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:

  • [ ] useUploadFile composable for file uploads
    • Progress tracking (progress ref with upload percentage)
    • Proper FormData handling
    • 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
        }
      }

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_size query 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 error ref 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 any types in public API)
  • Linting: ESLint with @fiscozen/eslint-config; run pnpm 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/core foundation
  • 🎯 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.