Spoosh
Plugin DevelopmentAdvanced

API

Providing custom methods via create()

api allows a plugin to provide custom methods through the create() return value, enabling client-level APIs instead of request-level hooks.

When to Use API

Featureinternalapi
Access locationPlugin context onlycreate() return value
Use casePlugin-to-plugin coordinationPublic client APIs
Runs per requestYesNo
Affects typingNoYes
Beginner-friendly

Good uses:

  • refetchAll() - Refetch all active queries
  • clearCache() - Clear all cache entries
  • prefetch() - Prefetch data before navigation
  • reset() - Reset client state
  • Devtools integration
  • Framework-specific adapters

Bad uses:

  • Request-specific logic (use middleware)
  • Logging (use afterResponse)
  • Auth headers (use middleware)
  • Caching decisions (use middleware)

Use api sparingly

Prefer middleware, afterResponse, or internal unless you intentionally want to extend the public client API.

setup vs api

These two work together:

Featuresetupapi
Runs whenOnce, when app startsOnce, when app starts
PurposeInitialize things (timers, listeners)Return methods for users
ReturnsNothingObject with methods

Think of it this way:

  • setup = Start the engine
  • api = Give the user the steering wheel

Example: GC Plugin

function gcPlugin(): SpooshPlugin {
  let runGc: () => void; // Will be set in setup

  return {
    name: "my-app:gc",
    operations: [],

    // setup: Initialize the cleanup logic
    setup({ stateManager }) {
      runGc = () => {
        // Clean old entries
        stateManager.clear();
      };

      // Run cleanup every minute
      setInterval(runGc, 60000);
    },

    // api: Let users trigger cleanup manually
    api: () => ({
      runGc() {
        runGc?.();
      },
    }),
  };
}

Usage:

const { runGc } = create(spoosh);

// Automatic cleanup happens every minute (from setup)
// User can also trigger it manually:
runGc();

Basic Example

function refetchPlugin(): SpooshPlugin {
  return {
    name: "core:refetch",
    operations: ["read", "write"],

    api: ({ eventEmitter }) => ({
      refetchAll() {
        eventEmitter.emit("refetchAll", undefined);
      },
    }),
  };
}

Usage:

const spoosh = new Spoosh("/api").use([refetchPlugin()]);
const { useRead, useWrite, refetchAll } = create(spoosh);

// Now you can call refetchAll
refetchAll();

Real-World Example: Cache Plugin

The cache plugin uses api to provide a clearCache method:

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

    middleware: async (context, next) => {
      // Cache logic...
      return next();
    },

    api(context) {
      const { stateManager, eventEmitter } = context;

      return {
        clearCache(options?: { refetchAll?: boolean }) {
          stateManager.clear();

          if (options?.refetchAll) {
            eventEmitter.emit("refetchAll", undefined);
          }
        },
      };
    },
  };
}

Usage:

const spoosh = new Spoosh("/api").use([cachePlugin()]);
const { useRead, clearCache } = create(spoosh);

// Clear all cache
clearCache();

// Clear and refetch all active queries
clearCache({ refetchAll: true });

API Context

The api function receives an ApiContext:

interface ApiContext {
  spoosh: SpooshApi; // Spoosh instance API
  stateManager: StateManager; // Cache and state access
  eventEmitter: EventEmitter; // Emit refetch/invalidate events
  pluginExecutor: PluginExecutor; // Plugin execution
}

Combining with Internal

You can use both api (for users) and internal (for other plugins):

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

    // For other plugins to use
    internal(context) {
      return {
        setRefetchMode(mode: "all" | "active") {
          context.temp.set("refetch:mode", mode);
        },
      };
    },

    // For users to use
    api(context) {
      return {
        refetchAll() {
          context.eventEmitter.emit("refetchAll", undefined);
        },
      };
    },
  };
}

Usage:

const spoosh = new Spoosh("/api").use([refetchPlugin()]);
const { useRead, refetchAll } = create(spoosh);

// Users call methods from create()
refetchAll();

// Other plugins use internal
context.plugins.get("spoosh:refetch").setRefetchMode("active");

TypeScript Support

To make api type-safe:

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

Step 1: Define Interface

// types.ts
export interface RefetchApi {
  refetchAll: () => void;
}

Step 2: Use in Plugin

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

export function refetchPlugin(): SpooshPlugin<{
  api: RefetchApi;
}> {
  return {
    name: "spoosh:refetch",
    operations: ["read", "write"],

    api: ({ eventEmitter }) => ({
      refetchAll() {
        eventEmitter.emit("refetchAll", undefined);
      },
    }),
  };
}

Step 3: TypeScript Knows the Types

const spoosh = new Spoosh("/api").use([refetchPlugin(), cachePlugin()]);
const hooks = create(spoosh);

// TypeScript knows these methods exist!
hooks.refetchAll(); // ✅ Type-safe
hooks.clearCache({ refetchAll: true }); // ✅ Type-safe

Real Example: Cache Plugin

// types.ts
export interface CacheApi {
  clearCache: (options?: ClearCacheOptions) => void;
}

// plugin.ts
export function cachePlugin(): SpooshPlugin<{
  readOptions: CacheReadOptions;
  writeOptions: CacheWriteOptions;
  readResult: CacheReadResult;
  writeResult: CacheWriteResult;
  api: CacheApi; // ← api types here
}> {
  return {
    name: "spoosh:cache",
    operations: ["read", "write"],

    api(context) {
      return {
        clearCache(options) {
          context.stateManager.clear();
          if (options?.refetchAll) {
            context.eventEmitter.emit("refetchAll", undefined);
          }
        },
      };
    },
  };
}

Multiple Plugins

When using multiple plugins, TypeScript automatically merges the types:

const spoosh = new Spoosh("/api").use([
  cachePlugin(), // Adds clearCache
  refetchPlugin(), // Adds refetchAll
]);

const hooks = create(spoosh);

// Both methods are available and type-safe
hooks.clearCache(); // ✅ From cachePlugin
hooks.refetchAll(); // ✅ From refetchPlugin

Best Practices

1. Prefer Simpler Alternatives

Before using api, ask:

  • Can this be middleware? (for request-specific logic)
  • Can this be afterResponse? (for side effects)
  • Can this be internal? (for plugin-to-plugin APIs)

Only use api when you need a public client-level API.

2. Keep APIs Minimal

Don't expose every internal function:

// ❌ Bad - Too many methods
api: () => ({
  clearCache() {
    /* ... */
  },
  getCacheSize() {
    /* ... */
  },
  getCacheEntry() {
    /* ... */
  },
  setCacheEntry() {
    /* ... */
  },
  deleteCacheEntry() {
    /* ... */
  },
});

// ✅ Good - Essential methods only
api: () => ({
  clearCache(options?) {
    /* ... */
  },
});

3. Use Clear Names

Instance methods become part of the public API:

// ❌ Bad - Unclear
spoosh.do();
spoosh.run();
spoosh.exec();

// ✅ Good - Self-explanatory
spoosh.prefetch();
spoosh.invalidateAll();
spoosh.clearCache();

4. Document Side Effects

If your method has side effects, document them:

api: () => ({
  /**
   * Clears all cache entries and optionally refetches active queries.
   *
   * @param options.refetchAll - If true, refetch all active queries
   */
  clearCache(options?: { refetchAll?: boolean }) {
    // ...
  },
});

Common Patterns

Pattern: Imperative Actions

For actions users trigger manually:

api: ({ eventEmitter }) => ({
  refetchAll() {
    eventEmitter.emit("refetchAll", undefined);
  },

  resetAll() {
    eventEmitter.emit("resetAll", undefined);
  },
});

Pattern: State Inspection

For debugging or devtools:

api: ({ stateManager }) => ({
  getDebugInfo() {
    return {
      cacheSize: stateManager.getSize(),
      queries: Array.from(stateManager.keys()),
    };
  },
});

Pattern: Framework Integration

For framework-specific utilities:

api: ({ stateManager, eventEmitter }) => ({
  getQueryData(queryKey: string) {
    const cached = stateManager.getCache(queryKey);
    return cached?.state.data;
  },

  setQueryData(queryKey: string, data: unknown) {
    stateManager.setCache(queryKey, {
      state: { data, error: undefined, timestamp: Date.now() },
      tags: [],
    });
  },
});

When NOT to Use

❌ Don't use for request-specific logic

// ❌ Bad - This should be middleware
api: () => ({
  addAuthHeader(token: string) {
    // This affects all requests - should be middleware
  },
});

// ✅ Good - Use middleware instead
middleware: async (context, next) => {
  context.request.headers.Authorization = `Bearer ${token}`;
  return next();
};

❌ Don't use for logging

// ❌ Bad - This should be afterResponse
api: () => ({
  enableLogging() {
    // This affects request flow - should be afterResponse
  },
});

// ✅ Good - Use afterResponse instead
afterResponse: (context, response) => {
  console.log(context.path, response.status);
};

❌ Don't use for plugin-to-plugin communication

// ❌ Bad - This should be internal
api: () => ({
  setRefetchMode(mode: string) {
    // Other plugins need this, not users
  },
});

// ✅ Good - Use internal instead
internal: (context) => ({
  setRefetchMode(mode: string) {
    context.temp.set("mode", mode);
  },
});

Summary

Use api when:

  • Building public client-level APIs (prefetch, invalidateAll)
  • Creating framework integrations
  • Providing imperative actions for users

Don't use api when:

  • Request-specific logic → use middleware
  • Side effects / logging → use afterResponse
  • Plugin-to-plugin APIs → use internal
  • Per-request state → use lifecycle

When in doubt, prefer simpler alternatives. api is powerful but should be treated as an advanced escape hatch.

On this page