Spoosh
Plugin DevelopmentAdvanced

Meta Storage

Storing user-facing data for hook results

setMeta allows plugins to store custom data that users can access via the meta property on hook results. This is for user-facing data only, not internal bookkeeping.

What is Meta?

Meta is additional data that plugins provide to users alongside the main response. Users access it through the meta property:

const read = injectRead((api) => api("users").GET());

// Plugins can store data here
console.log(read.meta().transformedData);
console.log(read.meta().summary);

Meta vs Response Data

  • Response data (data) - The actual API response
  • Meta (meta) - Additional plugin-provided data for users

Meta is cleared when the query refetches, just like the response data.

When to Use setMeta

✅ Good Use Cases

Transformed/Computed Data

afterResponse(context, response) {
  if (response.data) {
    const summary = {
      total: response.data.length,
      categories: groupByCategory(response.data),
      hasMore: response.data.length >= 100,
    };

    context.stateManager.setMeta(context.queryKey, {
      summary,
    });
  }
}

Usage:

const read = injectRead((api) => api("products").GET());

console.log(read.data()); // Original products array
console.log(read.meta().summary); // { total: 50, categories: {...}, hasMore: false }

Processed Results

afterResponse(context, response) {
  if (response.data) {
    const processed = expensiveComputation(response.data);

    context.stateManager.setMeta(context.queryKey, {
      processedData: processed,
    });
  }
}

Usage:

const read = injectRead((api) => api("items").GET());

// Use original data for display
<List [items]="read.data()" />

// Use processed data for calculations
const result = calculate(meta.processedData);

Validation Results

afterResponse(context, response) {
  if (response.data) {
    const validation = validateData(response.data);

    context.stateManager.setMeta(context.queryKey, {
      isValid: validation.isValid,
      errors: validation.errors,
      warnings: validation.warnings,
    });
  }
}

Usage:

const read = injectRead((api) => api("form").GET());

if (!read.meta().isValid) {
  console.error("Validation errors:", read.meta().errors);
}

Pagination Metadata

afterResponse(context, response) {
  const linkHeader = response.headers?.get?.("Link");

  if (linkHeader) {
    const pagination = parseLinkHeader(linkHeader);

    context.stateManager.setMeta(context.queryKey, {
      hasNextPage: !!pagination.next,
      hasPrevPage: !!pagination.prev,
      nextUrl: pagination.next,
      prevUrl: pagination.prev,
    });
  }
}

Usage:

const read = injectRead((api) => api("posts").GET());

<div>
  <Posts [items]="read.data()" />
  @if (read.meta().hasNextPage) {
    <button>Load More</button>
  }
</div>

❌ Bad Use Cases

Don't use for internal tracking

// ❌ Wrong - Users don't need this
afterResponse(context, response) {
  context.stateManager.setMeta(context.queryKey, {
    lastFetchedAt: Date.now(),      // Internal
    requestDuration: 123,            // Internal
    fromCache: false,                // Internal
  });
}

Don't use for plugin-to-plugin communication

// ❌ Wrong - Use internal or context.temp
afterResponse(context, response) {
  context.stateManager.setMeta(context.queryKey, {
    shouldRetry: true,  // Other plugins need this, not users
  });
}

// ✅ Right - Use internal
internal(context) {
  return {
    enableRetry() {
      context.temp.set("retry:enabled", true);
    },
  };
}

Don't use for response modification

// ❌ Wrong - Just return modified response
afterResponse(context, response) {
  context.stateManager.setMeta(context.queryKey, {
    data: { ...response.data, extra: "field" },
  });
}

// ✅ Right - Return modified response
afterResponse(context, response) {
  if (response.data) {
    return {
      ...response,
      data: { ...response.data, extra: "field" },
    };
  }
}

Real-World Example: Transform Plugin

The official transform plugin uses setMeta to store transformed data:

function transformPlugin(): SpooshPlugin {
  return {
    name: "spoosh:transform",
    operations: ["read", "write"],

    afterResponse: async (context, response) => {
      const pluginOptions = context.pluginOptions as TransformOptions;
      const transformer = pluginOptions?.transform;

      if (!transformer || response.data === undefined) {
        return;
      }

      const transformedData = await transformer(response.data);

      context.stateManager.setMeta(context.queryKey, {
        transformedData,
      });
    },
  };
}

Usage:

const read = injectRead((api) => api("posts").GET(), {
  transform: (posts) => ({
    total: posts.length,
    titles: posts.map((p) => p.title),
  }),
});

console.log(read.data()); // Original posts array
console.log(read.meta().transformedData); // { total: 10, titles: [...] }

TypeScript Support

To make meta type-safe, you need to:

  1. Define your meta interface
  2. Pass it as a generic to SpooshPlugin
  3. Export the types so users can import them

Step 1: Define Types

// types.ts
export interface SummaryMeta {
  summary: {
    total: number;
    categories: Record<string, number>;
  };
}

// Define result type that includes meta
export interface SummaryReadResult {
  meta: SummaryMeta;
}

Step 2: Use Types in Plugin

// plugin.ts
import type { SpooshPlugin } from "@spoosh/core";
import type { SummaryReadResult } from "./types";

export function summaryPlugin(): SpooshPlugin<{
  readResult: SummaryReadResult;
}> {
  return {
    name: "my-app:summary",
    operations: ["read"],

    afterResponse(context, response) {
      if (response.data) {
        context.stateManager.setMeta(context.queryKey, {
          summary: {
            total: response.data.length,
            categories: groupByCategory(response.data),
          },
        });
      }
    },
  };
}

Step 3: TypeScript Knows the Types

const spoosh = new Spoosh("/api").use([summaryPlugin()]);

// In your Angular component/service
const read = injectRead((api) => api("items").GET());

// TypeScript knows meta.summary exists!
console.log(read.meta().summary.total); // ✅ Type-safe
console.log(read.meta().summary.categories); // ✅ Type-safe

Real Example: Transform Plugin

The official transform plugin shows this pattern:

// types.ts
export interface TransformWriteResult {
  meta: {
    transformedData?: unknown;
  };
}

// plugin.ts
export function transformPlugin(): SpooshPlugin<{
  readOptions: TransformReadOptions;
  writeOptions: TransformWriteOptions;
  readResult: TransformReadResult;
  writeResult: TransformWriteResult; // ← Meta types here
}> {
  return {
    name: "spoosh:transform",
    operations: ["read", "write"],

    afterResponse: async (context, response) => {
      // Implementation...
      context.stateManager.setMeta(context.queryKey, {
        transformedData,
      });
    },
  };
}

Complete Plugin Type Structure

export function myPlugin(): SpooshPlugin<{
  // Per-request options users can pass
  readOptions: MyReadOptions;
  writeOptions: MyWriteOptions;
  pagesOptions: MyPagesOptions;

  // What gets added to hook results
  readResult: MyReadResult; // Includes meta
  writeResult: MyWriteResult; // Includes meta
  pagesResult: MyPagesResult;

  // What gets added to create() return
  api: MyInstanceApi;
}> {
  // Plugin implementation
}

Common Patterns

Pattern: Store Both Original and Processed

afterResponse(context, response) {
  if (response.data) {
    const normalized = normalizeData(response.data);
    const indexed = createIndex(normalized);

    context.stateManager.setMeta(context.queryKey, {
      normalized,  // For rendering
      indexed,     // For lookups
    });
  }
}

Usage:

const read = injectRead((api) => api("users").GET());

// Original data for raw access
const rawUsers = read.data();

// Normalized data for rendering
<UserList [users]="read.meta().normalized" />

// Indexed data for fast lookups
const user = read.meta().indexed[userId];

Pattern: Incremental Computation

afterResponse(context, response) {
  if (response.data) {
    const existing = context.stateManager.getMeta(context.queryKey);
    const previousCount = existing?.totalProcessed ?? 0;

    const newlyProcessed = processNewItems(response.data);

    context.stateManager.setMeta(context.queryKey, {
      totalProcessed: previousCount + newlyProcessed.length,
      lastProcessedAt: Date.now(),
    });
  }
}

Pattern: Enrichment

afterResponse(context, response) {
  if (response.data) {
    const enriched = {
      sentiment: analyzeSentiment(response.data),
      readingTime: calculateReadingTime(response.data),
      complexity: calculateComplexity(response.data),
    };

    context.stateManager.setMeta(context.queryKey, {
      analysis: enriched,
    });
  }
}

Usage:

const read = injectRead((api) => api("article/:id").GET({ params: { id } }));

<Article [content]="read.data()" />
<Sidebar>
  <p>Reading time: {{ read.meta().analysis.readingTime }} min</p>
  <p>Sentiment: {{ read.meta().analysis.sentiment }}</p>
</Sidebar>

Accessing Meta

In Angular

const read = injectRead((api) => api("users").GET());

console.log(read.meta().transformedData);
console.log(read.meta().summary);

In Angular

users = injectRead(() => this.api("users").GET());

// Access in template
{
  {
    users.meta()?.transformedData;
  }
}
{
  {
    users.meta()?.summary;
  }
}

// Access in component
const meta = this.users.meta();
console.log(meta?.transformedData);

Best Practices

1. Only Store User-Facing Data

// ✅ Good - Users need this
context.stateManager.setMeta(context.queryKey, {
  formattedData: formatForDisplay(response.data),
});

// ❌ Bad - Internal tracking
context.stateManager.setMeta(context.queryKey, {
  _internalFlag: true,
  _lastFetch: Date.now(),
});

2. Keep Meta Small

// ✅ Good - Minimal metadata
context.stateManager.setMeta(context.queryKey, {
  count: response.data.length,
  hasMore: response.data.length >= 100,
});

// ❌ Bad - Duplicating large data
context.stateManager.setMeta(context.queryKey, {
  entireDataset: response.data, // Already in response.data!
  duplicatedData: [...response.data],
});

3. Make It Optional

// ✅ Good - Meta can be undefined
const read = injectRead((api) => api("users").GET());

if (read.meta()?.summary) {
  console.log(read.meta().summary);
}

// ❌ Bad - Assuming meta exists
const count = meta.summary.total; // Might crash!

4. Document What You Store

/**
 * Stores summary metadata for product lists
 *
 * @meta summary.total - Total number of products
 * @meta summary.categories - Products grouped by category
 * @meta summary.hasMore - Whether more products are available
 */
function summaryPlugin(): SpooshPlugin {
  // ...
}

setMeta vs Returning Modified Response

ApproachUse When
Return modified responseModifying the main response data
setMetaAdding supplementary data alongside response

Return modified response:

afterResponse(context, response) {
  if (response.data) {
    return {
      ...response,
      data: {
        ...response.data,
        _timestamp: Date.now(),
      },
    };
  }
}

setMeta:

afterResponse(context, response) {
  if (response.data) {
    context.stateManager.setMeta(context.queryKey, {
      timestamp: Date.now(),
    });
  }
}

Use setMeta when:

  • Data is supplementary, not part of the response
  • Users might not always need it
  • It's computed/derived data
  • You want to keep response data clean

Return modified response when:

  • Modifying existing fields
  • Adding fields to the response object
  • Normalizing response structure

Summary

Use setMeta for:

  • ✅ Transformed/computed data users access
  • ✅ Validation results
  • ✅ Pagination metadata
  • ✅ Analysis/enrichment data
  • ✅ Supplementary information

Don't use setMeta for:

  • ❌ Internal tracking
  • ❌ Plugin-to-plugin communication (use internal or context.temp)
  • ❌ Response modification (return modified response instead)

Key points:

  • Meta is user-facing data only
  • Users access via meta property on hooks
  • Cleared on refetch (like response data)
  • Keep it small and optional

For more on afterResponse, see After Response.

For plugin-to-plugin communication, see Plugin Communication.

On this page