Spoosh
Plugin Development

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
  • afterResponse runs 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

FeaturemiddlewareafterResponse
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 forCache, retry, deduplicationLogging, 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!");
    },
  };
}

That's it! The message appears once when your app starts. You can use setup to initialize global state, configure services, or log startup info.

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
  • API - Extending the Spoosh instance with custom methods (advanced)
  • Testing - Unit tests, integration tests, test utilities

Check out the official plugins for real-world examples.

On this page