Spoosh

Introduction

A type-safe API toolkit with automatic cache invalidation and zero boilerplate

Spoosh is a type-safe API toolkit with automatic cache management. Define your API schema once, then get type-safe requests with automatic cache invalidation based on URL hierarchy.

// 1. Define your schema (or infer it from Hono / Elysia / OpenAPI)
type API = {
  users: { GET: { data: User[]; query: { page?: number } } };
  "users/:id": {
    GET: { data: User };
    PUT: { data: User; body: Partial<User> };
  };
  "users/:id/posts": {
    GET: { data: Post[] };
    POST: { data: Post; body: NewPost };
  };
};

// 2. Create the client
const spoosh = new Spoosh<API, Error>("/api").use([
  cachePlugin({ staleTime: 5 * 60 * 1000 }),
  deduplicationPlugin(),
  invalidationPlugin(),
  // ...other plugins
]);

// 3. Use in components — paths are fully typed from your schema
export const { injectRead, injectWrite } = create(spoosh);

Using Hono or Elysia? You can infer the schema directly from your server routes for end-to-end type safety with zero manual definitions.

What Makes Spoosh Different

Automatic Cache Invalidation

Spoosh uses a tag-based caching system. Each query is automatically tagged based on its resolved API path:

// Tags are generated from the resolved path:
injectRead((api) => api("users").GET());
// → tag: "users"

injectRead((api) => api("users/:id").GET({ params: { id: 123 } }));
// → tag: "users/123"

injectRead((api) => api("users/:id/posts").GET({ params: { id: 123 } }));
// → tag: "users/123/posts"

When you mutate data, Spoosh automatically invalidates related queries using wildcard pattern matching.

write = injectWrite((api) => api("users/:id/posts").POST());

// Default invalidation pattern: ["users", "users/*"]
// Matches: "users", "users/123", "users/123/posts", etc.
await write.trigger({ params: { id: 123 }, body: newPost });

Type-Safe Routing with Zero Strings

Define your API schema once, then use a function-based API that gives you autocomplete for every endpoint, parameter, and response.

// Fully typed paths
user = injectRead((api) => api("users/:id").GET({ params: { id: 123 } }));
posts = injectRead((api) =>
  api("users/:id/posts").GET({ params: { id: 123 }, query: { page: 1 } })
);

TypeScript knows every endpoint exists, every parameter is correct, and every response type matches your schema.

Composable Plugin System

Spoosh has a rich plugin ecosystem. Each plugin adds specific functionality, and TypeScript knows exactly what options are available based on which plugins you install.

import { Spoosh } from "@spoosh/core";
import { cachePlugin } from "@spoosh/plugin-cache";
import { retryPlugin } from "@spoosh/plugin-retry";
import { pollingPlugin } from "@spoosh/plugin-polling";

const spoosh = new Spoosh<API, Error>("/api").use([
  cachePlugin({ staleTime: 5000 }),
  retryPlugin({ retries: 3 }),
  pollingPlugin({ interval: 30000 }),
]);

// Options are typed based on installed plugins
injectRead((api) => api("users").GET(), {
  staleTime: 10000, // ✓ Known from cachePlugin
  retry: { retries: 5 }, // ✓ Known from retryPlugin
  interval: 60000, // ✓ Known from pollingPlugin
  // randomOption: true  // ✗ Type error
});

Browse the full list of available plugins on the Plugins page.

Next Steps

On this page