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
| Feature | internal | api |
|---|---|---|
| Access location | Plugin context only | create() return value |
| Use case | Plugin-to-plugin coordination | Public client APIs |
| Runs per request | Yes | No |
| Affects typing | No | Yes |
| Beginner-friendly | ✅ | ❌ |
Good uses:
refetchAll()- Refetch all active queriesclearCache()- Clear all cache entriesprefetch()- Prefetch data before navigationreset()- 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:
| Feature | setup | api |
|---|---|---|
| Runs when | Once, when app starts | Once, when app starts |
| Purpose | Initialize things (timers, listeners) | Return methods for users |
| Returns | Nothing | Object with methods |
Think of it this way:
setup= Start the engineapi= 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:
- Define your api interface
- Pass it as a generic to
SpooshPlugin - 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-safeReal 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 refetchPluginBest 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.