Plugin Development
Create custom plugins for Spoosh
Plugins add features to Spoosh. They run before or after requests to add functionality like authentication, logging, or caching.
Your First Plugin
Here's a simple plugin that logs every request:
import type { SpooshPlugin } from "@spoosh/core";
function loggerPlugin(): SpooshPlugin {
return {
name: "my-app:logger",
operations: ["read", "write"],
middleware: async (context, next) => {
console.log("Request:", context.path);
const response = await next();
console.log("Response:", response.status);
return response;
},
};
}How to Use It
const spoosh = new Spoosh("/api").use([loggerPlugin()]);That's it! Your plugin will now log all requests and responses.
Type-Safe Plugins
For plugins with options, use the createSpooshPlugin helper for automatic type inference. See the Type Safety guide for details.
How Plugins Work
Every plugin has:
name- A unique identifier (use"your-app:plugin-name"format)operations- Which operations it handles ("read","write","pages")middleware- A function that runs during requests (optional)afterResponse- A function that runs after every response (optional)
The middleware function:
- Receives
context(info about the request) - Calls
next()to continue to the next plugin or make the actual request - Can modify the request before calling
next() - Can modify the response after
next()returns - Can skip
next()to return early
The afterResponse function:
- Always runs after the request completes
- Can return a modified response or void for side effects
- Perfect for logging, analytics, and transforming responses
The Onion Model
Plugins wrap each other like layers of an onion. Each middleware wraps the next:
┌─────────────────────────────────────┐
│ Plugin A: Start │
│ ┌───────────────────────────────┐ │
│ │ Plugin B: Start │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Plugin C: Start │ │ │
│ │ │ ┌───────────────────┐ │ │ │
│ │ │ │ ACTUAL FETCH │ │ │ │
│ │ │ └───────────────────┘ │ │ │
│ │ │ Plugin C: End │ │ │
│ │ └─────────────────────────┘ │ │
│ │ Plugin B: End │ │
│ └───────────────────────────────┘ │
│ Plugin A: End │
└─────────────────────────────────────┘What this means:
- Plugin A runs first (before fetch)
- Plugin A's code after
await next()runs last (after fetch) - Inner plugins can return early — outer plugins' middleware will not resume
afterResponseruns for all plugins after a response is produced, even if middleware returned early (e.g. from cache)
Example flow:
const spoosh = new Spoosh("/api").use([
cachePlugin(), // Plugin A
retryPlugin(), // Plugin B
loggerPlugin(), // Plugin C
]);
// Request flow:
// 1. cachePlugin middleware starts
// 2. Cache miss, calls next()
// 3. retryPlugin middleware starts
// 4. Calls next()
// 5. loggerPlugin middleware starts
// 6. Calls next()
// 7. ACTUAL FETCH happens
// 8. loggerPlugin middleware ends
// 9. retryPlugin middleware ends
// 10. cachePlugin middleware ends
// 11. ALL afterResponse hooks run (cache, retry, logger)middleware vs afterResponse
| Feature | middleware | afterResponse |
|---|---|---|
| Can prevent request | ✅ Yes (return early without calling next) | ❌ No (response already exists) |
| Always runs | ❌ No (skipped if earlier plugin returns) | ✅ Yes (runs even if cache returned) |
| Can modify response | ✅ Yes | ✅ Yes |
| Best for | Cache, retry, deduplication | Logging, analytics, adding metadata |
Rule of thumb:
- If you need control flow → use
middleware - If you need guarantees → use
afterResponse
Essential Middleware Patterns
Pattern 1: Do Something Before and After
Run code before and after a request:
function timingPlugin(): SpooshPlugin {
return {
name: "my-app:timing",
operations: ["read", "write"],
middleware: async (context, next) => {
const start = Date.now();
const response = await next();
const duration = Date.now() - start;
console.log(`Request took ${duration}ms`);
return response;
},
};
}Pattern 2: Return Early
Skip the actual request by returning without calling next():
function mockPlugin(): SpooshPlugin {
return {
name: "my-app:mock",
operations: ["read"],
middleware: async (context, next) => {
if (context.path === "users/123") {
return {
data: { id: "123", name: "Mock User" },
status: 200,
};
}
return next();
},
};
}Important: Middleware must return a valid SpooshResponse when returning early (with data or error and status).
Pattern 3: React to Responses
Use afterResponse for side effects or to transform responses:
// Example 1: Side effects (logging)
function analyticsPlugin(): SpooshPlugin {
return {
name: "my-app:analytics",
operations: ["read", "write"],
afterResponse(context, response) {
trackEvent("api_request", {
path: context.path,
method: context.method,
status: response.status,
success: !response.error,
});
},
};
}
// Example 2: Transform response
function timestampPlugin(): SpooshPlugin {
return {
name: "my-app:timestamp",
operations: ["read"],
afterResponse(context, response) {
if (response.data) {
return {
...response,
data: {
...response.data,
fetchedAt: Date.now(),
},
};
}
},
};
}Why use afterResponse?
- Always runs, even if cache returned early
- Can return modified response or void for side effects
- Perfect for logging, analytics, adding metadata
Pattern 4: Clean Up Resources
Use lifecycle hooks to clean up when components unmount:
function trackingPlugin(): SpooshPlugin {
const activeRequests = new Set<string>();
return {
name: "my-app:tracking",
operations: ["read"],
middleware: async (context, next) => {
activeRequests.add(context.queryKey);
const response = await next();
activeRequests.delete(context.queryKey);
return response;
},
lifecycle: {
onUnmount(context) {
activeRequests.delete(context.queryKey);
},
},
};
}One-Time Setup
Sometimes your plugin needs to do something once when the app starts. Use setup for this:
function startupLoggerPlugin(): SpooshPlugin {
return {
name: "my-app:startup-logger",
operations: ["read", "write"],
setup() {
console.log("🚀 Spoosh is ready!");
},
};
}Sometimes your plugin needs to do something once when the app starts. Use setup for this:
Plugin Properties
Here are the main properties you can use in a plugin:
{
name: string; // Required: unique identifier
operations: string[]; // Required: ["read", "write", "pages"]
priority?: number; // Optional: execution order (lower runs first)
setup?: Function; // Run once when app starts
middleware?: Function; // Intercept requests
afterResponse?: Function; // Run after responses
lifecycle?: { // Handle component lifecycle
onMount?: Function;
onUpdate?: Function;
onUnmount?: Function;
};
internal?: Function; // Provide APIs to other plugins
api?: Function; // Add methods to create() result
}What's Next?
Want to learn more advanced techniques?
- Patterns - Middleware patterns, retry logic, caching strategies
- After Response - Side effects and transformations that always run
- Meta Storage - Storing user-facing data for hook results
- Plugin Communication - How plugins share data and coordinate
- Tracing - Emit trace events for devtool visibility
- Lifecycle - Managing state, tracking instances, cleanup
- Architecture - How plugins work internally, execution order, PluginContext
- Type Safety - Making your plugins type-safe with TypeScript
- Instance APIs - Extending the Spoosh instance with custom methods (advanced)
- Testing - Unit tests, integration tests, test utilities
Check out the official plugins for real-world examples.