Spoosh
Plugins

Transform

Transform response data with full type inference

The Transform plugin enables per-request transformation of response data with full type inference.

Installation

npm install @spoosh/plugin-transform

Setup

import { Spoosh } from "@spoosh/core";
import { transformPlugin } from "@spoosh/plugin-transform";

const spoosh = new Spoosh<ApiSchema, Error>("/api").use([transformPlugin()]);

Usage

Response Transforms (useRead)

Response transforms produce a separate transformedData field in meta while preserving the original data:

const { data, meta } = useRead((api) => api("posts").GET(), {
  transform: (posts) => ({
    count: posts.length,
    hasMore: posts.length >= 10,
    ids: posts.map((p) => p.id),
  }),
});

// data = Post[] (original response, preserved)
// meta.transformedData = { count: number, hasMore: boolean, ids: number[] } | undefined

The meta.transformedData type is automatically inferred from your transformer's return type.

Response Transforms (useWrite)

With hook-level options, response transforms for useWrite provide full type inference for meta.transformedData:

const { trigger, data, meta } = useWrite((api) => api("posts").POST(), {
  transform: (post) => ({
    success: true,
    postId: post.id,
    createdAt: new Date(post.timestamp),
  }),
});

await trigger({ body: { title: "New Post" } });

// After trigger completes:
// data = Post (original response, preserved)
// meta.transformedData = { success: boolean, postId: number, createdAt: Date } | undefined

By passing transform as a hook-level option (second argument to useWrite), TypeScript can properly infer the transformedData type.

Response Transform Examples

Transform and Analyze Data

Transform and analyze response data:

const { data, meta } = useRead((api) => api("analytics").GET(), {
  transform: (analytics) => ({
    totalViews: analytics.reduce((sum, a) => sum + a.views, 0),
    averageEngagement:
      analytics.reduce((sum, a) => sum + a.engagement, 0) / analytics.length,
    topPerformer: analytics.sort((a, b) => b.views - a.views)[0],
  }),
});

// data = original analytics array
// meta.transformedData = computed summary with totals and top performer

Async Transforms

All transform functions support async operations:

const { data, meta } = useRead((api) => api("posts").GET(), {
  transform: async (posts) => {
    const enriched = await enrichPostsWithMetadata(posts);
    return {
      count: enriched.length,
      titles: enriched.map((p) => p.title),
    };
  },
});

Removing Data

Return undefined to remove the data entirely:

const { data, meta } = useRead((api) => api("posts").GET(), {
  transform: (posts) => {
    if (posts.length === 0) return undefined;
    return {
      count: posts.length,
      titles: posts.map((p) => p.title),
    };
  },
});

Type Inference

The transform plugin provides full type inference:

// Response type is inferred from endpoint (useRead)
const { data, meta } = useRead((api) => api("users").GET(), {
  transform: (users) => {
    // 'users' is typed as User[]
    return {
      activeCount: users.filter((u) => u.active).length,
      admins: users.filter((u) => u.role === "admin"),
    };
  },
});

// meta.transformedData is typed as:
// { activeCount: number; admins: User[] } | undefined

Type Inference with Hook-Level Options

By passing transform as a hook-level option, meta.transformedData is properly typed:

const { trigger, meta } = useWrite((api) => api("posts").POST(), {
  transform: (post) => ({
    success: true,
    postId: post.id,
  }),
});

await trigger({ body: { title: "New Post" } });

// meta.transformedData is properly typed as:
// { success: boolean; postId: number } | undefined
console.log(meta.transformedData?.success); // ✓ Works with full type safety

Use Cases

Compute Derived Data

const { data, meta } = useRead((api) => api("orders").GET(), {
  transform: (orders) => ({
    total: orders.reduce((sum, o) => sum + o.amount, 0),
    averageOrder: orders.reduce((sum, o) => sum + o.amount, 0) / orders.length,
    pendingCount: orders.filter((o) => o.status === "pending").length,
  }),
});

Extract Specific Fields

const { data, meta } = useRead((api) => api("posts").GET(), {
  transform: (posts) => ({
    ids: posts.map((p) => p.id),
    titles: posts.map((p) => p.title),
    publishedCount: posts.filter((p) => p.published).length,
  }),
});

Enrich Response Data

const { data, meta } = useRead((api) => api("users").GET(), {
  transform: (users) => ({
    totalUsers: users.length,
    activeUsers: users.filter((u) => u.active),
    usersByRole: users.reduce(
      (acc, u) => {
        acc[u.role] = (acc[u.role] || 0) + 1;
        return acc;
      },
      {} as Record<string, number>
    ),
  }),
});

On this page