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.

What Makes Spoosh Different

Hierarchical Cache Invalidation

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

// Query tags are automatically generated from the path:
injectRead((api) => api("users").GET());
// → tags: ["users"]

injectRead((api) => api("users/:id").GET({ params: { id: 123 } }));
// → tags: ["users", "users/123"]

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

When you mutate data, Spoosh automatically invalidates related queries based on these tags. Create a post for user 123? Every query that depends on that user's data refreshes automatically.

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

// This mutation automatically invalidates:
// - users
// - users/123
// - users/123/posts
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.

Custom Tags

You can also specify custom tags manually using the unified tags option:

// Mode only - 'all' generates full hierarchy
posts = injectRead(
  (api) => api("users/:id/posts").GET({ params: { id: "123" } }),
  {
    tags: "all", // ['users', 'users/123', 'users/123/posts']
  }
);

// Mode only - 'self' generates only exact path
posts = injectRead(
  (api) => api("users/:id/posts").GET({ params: { id: "123" } }),
  {
    tags: "self", // ['users/123/posts']
  }
);

// Custom tags only - replaces auto-generated tags
users = injectRead((api) => api("users").GET(), {
  tags: ["custom-users", "dashboard"], // ['custom-users', 'dashboard']
});

// Mode + custom tags - 'all' mode combined with custom tags
posts = injectRead(
  (api) => api("users/:id/posts").GET({ params: { id: "123" } }),
  {
    tags: ["all", "dashboard"], // ['users', 'users/123', 'users/123/posts', 'dashboard']
  }
);

// Mode + custom tags - 'self' mode combined with custom tags
posts = injectRead(
  (api) => api("users/:id/posts").GET({ params: { id: "123" } }),
  {
    tags: ["self", "dashboard"], // ['users/123/posts', 'dashboard']
  }
);

// On mutations, specify which tags to invalidate
createPost = injectWrite((api) => api("posts").POST);

await createPost.trigger({
  body: { title: "New Post" },
  invalidate: ["posts", "stats", "custom-tag"],
});

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
  retries: 5, // ✓ Known from retryPlugin
  interval: 60000, // ✓ Known from pollingPlugin
  // randomOption: true  // ✗ Type error
});

Works with Any REST API

Spoosh works with any REST API. Use it with Express, Hono, Elysia, or any backend framework you prefer.

With Hono and Elysia adapters, you get automatic type inference from your server routes with zero configuration.

Lightweight & Fast

At ~10KB gzipped for the core, Spoosh keeps your bundle size minimal. Add only the plugins you need to extend functionality.

Packages

PackageDescription
@spoosh/coreCore client and plugin system
@spoosh/angularAngular bindings (injectRead, injectWrite, injectInfiniteRead)
@spoosh/honoHono type adapter
@spoosh/elysiaElysia type adapter
@spoosh/openapiBidirectional OpenAPI converter (import/export)

Plugins

PluginDescription
@spoosh/plugin-cacheResponse caching with stale time
@spoosh/plugin-invalidationAuto-refresh queries after mutations (recommended)
@spoosh/plugin-retryAutomatic retry with configurable attempts
@spoosh/plugin-optimisticOptimistic updates with rollback
@spoosh/plugin-pollingAutomatic polling at intervals
@spoosh/plugin-debounceRequest debouncing
@spoosh/plugin-throttleRequest throttling
@spoosh/plugin-deduplicationPrevents duplicate in-flight requests
@spoosh/plugin-prefetchPreload data before needed
@spoosh/plugin-refetchRefetch on focus/reconnect
@spoosh/plugin-initial-dataShow data before fetch completes
@spoosh/plugin-gcGarbage collection for cache management
@spoosh/plugin-transformTransform request and response data
@spoosh/plugin-qsQuery string serialization for nested objects
@spoosh/plugin-progressUpload/download progress tracking via XHR
@spoosh/plugin-nextjsNext.js server revalidation
@spoosh/plugin-debugDebug logging

Next Steps

On this page