npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

t7m

v1.0.0

Published

Transformer for Elysia and Hono

Readme

t7m

npm version TypeScript 5+ license

APIs shouldn't return raw database models. Sensitive fields leak, related data loads slowly, and the same async call runs over and over.

t7m is a transformer layer that fixes this: one class per model controls what gets exposed, loads includes in parallel, and caches repeated calls.

Works with Hono and Elysia. No overhead — 1,000 objects with includes in under 100ms.

t7m = t(ransfor)m - 7 letters between t and m

AI Agent Skill

t7m ships with a Claude Code skill that teaches AI coding agents how to build transformers correctly — includes, cache, props, nested transformers, and framework integration.

Install it with the Vercel Skills CLI:

npx skills add tkoehlerlg/t7m

This works with Claude Code, Cursor, Codex, and other agents that support the open agent skills standard.

Quick Start

npm install t7m
# or
bun add t7m
import { AbstractTransformer } from 't7m';

type User = { id: number; name: string; email: string; password: string };
type PublicUser = Omit<User, "id" | "password">;

class UserTransformer extends AbstractTransformer<User, PublicUser> {
  data(input: User): PublicUser {
    return { name: input.name, email: input.email };
  }
}

const transformer = new UserTransformer();
const user: User = { id: 1, name: "Alice", email: "[email protected]", password: "secret" };
const result = await transformer.transform({ input: user });
// { name: "Alice", email: "[email protected]" } - sensitive fields stripped!

Why t7m?

The Problem

Database models contain sensitive data you shouldn't expose (IDs, passwords, internal flags). Every API endpoint needs to strip fields, optionally include related data, and do this consistently. Without structure, transformation logic scatters across your codebase - easy to forget a field, expose something you shouldn't, or handle includes inconsistently.

The Solution

t7m gives you a single place to define how each model transforms to its public form. Type-safe, consistent, with built-in support for optional includes and caching. Works anywhere - built with serverless in mind.

Basic Usage

Defining a Transformer

Extend AbstractTransformer and implement the data method:

import { AbstractTransformer } from 't7m';

interface User {
  id: number;
  name: string;
  email: string;
}

type PublicUser = Omit<User, "id">;

// AbstractTransformer<Input, Output>
class UserTransformer extends AbstractTransformer<User, PublicUser> {
  data(input: User): PublicUser {
    return {
      name: input.name,
      email: input.email,
    };
  }
}

const transformer = new UserTransformer();

const user: User = { id: 1, name: "John Doe", email: "[email protected]" };
const publicUser = await transformer.transform({ input: user });
// { name: 'John Doe', email: '[email protected]' }

Includes

Includes let you optionally add related data to your output (like posts for a user, or author for a comment). Define handlers in includesMap - they only run when requested. All include functions run in parallel.

// Third generic = Props type (passed to data and include functions)
interface UserWithPosts extends PublicUser {
  posts?: { title: string }[];
}

class UserTransformer extends AbstractTransformer<User, UserWithPosts, { db: Database }> {
  // data() can be async
  async data(input: User): Promise<UserWithPosts> {
    return { name: input.name, email: input.email };
  }

  includesMap = {
    posts: async (input: User, props) =>
      new PostTransformer().transformMany({ inputs: await props.db.getPostsByUserId(input.id) }),
  };
}

const transformer = new UserTransformer();
const publicUser = await transformer.transform({
  input: user,
  includes: ["posts"],
  props: { db },
});
// { name: "John", email: "...", posts: [{ title: "Hello" }, ...] }

Props

Props are available in both data(input, props) and include functions (input, props, forwardedIncludes). Common uses:

  • Database connections
  • Feature flags (e.g., redactSensitiveData: boolean)
  • Request context

When your transformer defines a Props type, props becomes required in transform() and transformMany(). When no Props type is defined (the default), props cannot be passed.

Unsafe Includes

For dynamic includes from user input (e.g., query strings), use unsafeIncludes. They're not type-checked but handled gracefully at runtime:

await transformer.transform({
  input: user,
  includes: ["posts"],           // Type-safe
  unsafeIncludes: queryIncludes, // Runtime includes
  props: { db },
});

t7m automatically deduplicates includes. Unhandled includes (not in your includesMap) are passed to include functions as forwardedIncludes, so you can forward them to nested transformers:

includesMap = {
  posts: async (input: User, props, forwardedIncludes) =>
    new PostTransformer().transformMany({
      inputs: await props.db.getPostsByUserId(input.id),
      unsafeIncludes: forwardedIncludes, // Forward "author", "comments", etc.
    }),
};

// Request includes: ["posts", "author"]
// → "posts" handled by UserTransformer (this includesMap)
// → "author" not in UserTransformer's includesMap, so forwarded to PostTransformer

Type Utilities

t7m exports utility types for extracting type information from transformer instances. Useful for writing generic functions and framework integrations.

| Type | Description | |------|-------------| | AnyAbstractTransformer | Base type for typing transformer collections and generic utilities | | InputOf<T> | Extract the input type from a transformer | | OutputOf<T> | Extract the output type from a transformer | | PropsOf<T> | Extract the props type from a transformer | | IncludesOf<T> | Extract the available include keys from a transformer |

import type { InputOf, OutputOf } from 't7m';

type UserInput = InputOf<UserTransformer>;   // User
type UserOutput = OutputOf<UserTransformer>; // PublicUser

Cache

When transforming data, you often need to enrich it with external information. Cache wraps any function and ensures calls with the same input resolve only once. Concurrent calls share the same promise - no duplicate requests, no race conditions.

Keep in mind:

  • Cache lives on the transformer instance — reuse one instance per request, don't create a new one each call
  • There's no TTL. The framework middleware (Hono/Elysia) clears caches after each response; when calling transform()/transformMany() directly, call clearCache() yourself (see Cache Auto-Clear)

Basic Usage

import { AbstractTransformer, Cache } from 't7m';

class CommentTransformer extends AbstractTransformer<Comment, PublicComment> {
  cache = {
    userProfile: new Cache((userId: string) => auth.getUser(userId)),
  };

  data(input: Comment): PublicComment {
    return { id: input.id, content: input.content };
  }

  includesMap = {
    author: async (input: Comment) => {
      // Cached! 20 comments with same userId = 1 auth call
      const user = await this.cache.userProfile.call(input.userId);
      return { name: user.name, avatarUrl: user.picture };
    },
  };
}

// 100 comments, 20 unique users = only 20 auth calls!
const transformer = new CommentTransformer();
await transformer.transformMany({ inputs: comments, includes: ["author"] });

Zero-Argument Functions

Cache supports 0-arg functions - useful for deferring transformer instantiation (e.g., to avoid circular dependencies or reduce startup cost):

class ParentTransformer extends AbstractTransformer<Parent, PublicParent> {
  transformers = {
    child: new Cache(() => new ChildTransformer()),
  };

  includesMap = {
    children: (input) => this.transformers.child.call().transformMany({ inputs: input.children }),
  };
}

Object Arguments and Selective Keys

For object arguments, specify which keys to use for the cache key:

const cached = new Cache(
  (params: { id: number; timestamp: number }) => db.users.findOne({ id: params.id }),
  { on: ["id"] } // Only cache on 'id', ignore 'timestamp'
);

await cached.call({ id: 1, timestamp: 100 });
await cached.call({ id: 1, timestamp: 200 }); // Cache hit!

You can specify multiple keys: new Cache(fn, { on: ["id", "type"] })

Limit cache size with maxSize: new Cache(fn, { maxSize: 100 })

Cache Auto-Clear

By default, caches clear after each transformation. Disable auto-clear with:

class MyTransformer extends AbstractTransformer<Input, Output> {
  constructor() {
    super({ clearCacheOnTransform: false });
  }
}

Nested Transformer Cache Clearing

Register nested transformers in transformers for cache clearing propagation. Parent clears all caches only after transformation completes - handled internally:

class PostTransformer extends AbstractTransformer<Post, PublicPost> {
  authorTransformer = new AuthorTransformer();

  transformers = { author: this.authorTransformer };

  includesMap = {
    author: async (input) => this.authorTransformer.transform({ input: await getAuthor(input.authorId) }),
  };
}

Circular references between transformers are handled safely - cache clearing uses cycle detection to prevent infinite loops.

Framework Integration

Hono

Setup

import { Hono } from 'hono';
import { t7mMiddleware } from 't7m/hono';

const app = new Hono();
app.use(t7mMiddleware);

Basic route usage:

app.get("/users", async (c) => {
  const users = await db.users.findMany();
  return c.transformMany(users, new UserTransformer(), {}, 200);
  // c.transform(user, new UserTransformer(), {}, 200) for single objects
});

Automatic Query Parameter Parsing

The middleware automatically reads ?include= from the query string and passes them as includes to the transformer.

GET /users?include=posts,comments
// Automatically applies includes: ["posts", "comments"]

No additional code needed - just use the middleware.

Extras

The third parameter (extras) is always required — pass {} when you don't need any options. It supports these options:

| Option | Type | Description | |--------|------|-------------| | includes | IncludesOf<T>[] | Type-safe includes (used instead of query params) | | wrapper | (data) => O | Wrap the response (e.g., { data: result }) | | debug | boolean | Enable colored console logging for debugging | | props | PropsOf<T> | Props to pass to the transformer |

Example with wrapper and debug:

app.get("/users", async (c) => {
  const users = await db.users.findMany();
  return c.transformMany(users, new UserTransformer(), {
    wrapper: (data) => ({ data, count: data.length }),
    debug: true,
  }, 200);
});

Custom HTTP headers can be passed as the 5th parameter.

Elysia

Setup

import { Elysia } from 'elysia';
import { t7mPlugin } from 't7m/elysia';

const app = new Elysia();
app.use(t7mPlugin());

The plugin injects transform() and transformMany() into every route handler via Elysia's derive.

Basic route usage:

app.get("/users", async ({ transformMany }) => {
  const users = await db.users.findMany();
  return transformMany(users, new UserTransformer());
  // transform(user, new UserTransformer()) for single objects
});

Automatic Query Parameter Parsing

The plugin automatically reads ?include= from the query string and passes them as includes to the transformer.

GET /users?include=posts,comments
// Automatically applies includes: ["posts", "comments"]

No additional code needed - just use the plugin.

Extras

The third parameter (extras) supports these options:

| Option | Type | Description | |--------|------|-------------| | includes | IncludesOf<T>[] | Type-safe includes (used instead of query params) | | wrapper | (data) => O | Wrap the response (e.g., { data: result }) | | debug | boolean | Enable colored console logging for debugging | | props | PropsOf<T> | Props to pass to the transformer |

The extras parameter is optional when your transformer has no Props type — no empty {} needed (unlike Hono).

Example with wrapper and debug:

app.get("/users", async ({ transformMany }) => {
  const users = await db.users.findMany();
  return transformMany(users, new UserTransformer(), {
    wrapper: (data) => ({ data, count: data.length }),
    debug: true,
  });
});

Key Difference from Hono

Elysia handlers return plain data — the plugin's transform() returns the transformed object directly. For status codes and headers, use Elysia's set:

app.get("/users/:id", async ({ transform, set, params }) => {
  const user = await db.users.findOne(params.id);
  if (!user) {
    set.status = 404;
    return { error: "Not found" };
  }
  set.headers["X-Custom-Header"] = "value";
  return transform(user, new UserTransformer());
});

Performance

Parallel Includes

All include functions run concurrently via Promise.all. If you have 3 includes that each take 50ms, the total is ~50ms, not 150ms.

Cache Deduplication

Cache eliminates redundant calls by sharing the same promise across concurrent lookups:

100 comments, 20 unique authors
├─ Without cache:  100 auth.getUser() calls
└─ With cache:      20 auth.getUser() calls (5x reduction)

Concurrent calls with the same input don't even wait — they share a single in-flight promise, so there are no race conditions and no duplicate work.

Benchmarks (from test suite)

| Scenario | Result | |----------|--------| | 1,000 objects + 2 includes each | < 100ms | | 10,000 cached primitive lookups | microseconds per lookup | | Cached vs uncached (1ms async op, 1,000 calls) | ~1ms cached vs ~1,000ms uncached |

Concurrency Control

The Problem

transformMany fires all items and their includes in parallel via Promise.all. With 100 items and 2 includes each, that's up to 200 concurrent external calls in a single request. This overwhelms services that weren't designed for that kind of burst:

  • Cloudflare Workers: Hard limit of 6 concurrent subrequests per request — anything beyond throws
  • Auth providers (Clerk, Auth0): Rate limits on API calls that trigger 429s under burst traffic
  • Third-party APIs: Connection ceilings, per-second rate limiting, or socket exhaustion

When You Don't Need This

Concurrency control is opt-in. You can skip it entirely when:

  • Database with connection pooling (e.g., Neon): The pool manages concurrency for you — includes all go through one pooled connection, so there's no flood of parallel connections
  • In-memory lookups: No external calls means no limits to hit
  • Already-cached calls: If you're using t7m's Cache, duplicate calls are deduplicated — 100 items with 20 unique authors = 20 actual calls, not 100

Item-Level Concurrency

The concurrency constructor parameter limits how many items transformMany / _transformMany process in parallel:

class CommentTransformer extends AbstractTransformer<Comment, PublicComment> {
  constructor() {
    super({ concurrency: 5 }) // Process at most 5 items at a time
  }

  // ...
}

// 100 comments — processed in batches of 5 instead of all at once
await transformer.transformMany({ inputs: comments, includes: ['author'] })

This does not apply to transform / _transform (single item — nothing to throttle).

Per-Include Concurrency

The includesConcurrency class property limits how many times a specific include function can run concurrently. Unlike concurrency, per-include limits apply to all transform methods — both transform() and transformMany(). Include keys not listed remain unlimited:

class CommentTransformer extends AbstractTransformer<Comment, PublicComment> {
  includesConcurrency = {
    author: 3, // At most 3 concurrent author lookups across all items
  }

  includesMap = {
    author: async (input: Comment) => {
      const user = await auth.getUser(input.userId)
      return { name: user.name }
    },
    tags: async (input: Comment) => {
      // No limit — runs with full parallelism
      return await db.getTagsForComment(input.id)
    },
  }
}

The semaphore is shared across all calls on the same transformer instance, so even if multiple transformMany calls overlap, the limit holds.

Combined Example

Use both together for fine-grained control:

class CommentTransformer extends AbstractTransformer<Comment, PublicComment, { db: Database }> {
  constructor() {
    super({ concurrency: 10 }) // 10 items in parallel
  }

  cache = {
    userProfile: new Cache((userId: string) => auth.getUser(userId)),
  }

  includesConcurrency = {
    author: 5,  // Max 5 concurrent auth calls
  }

  data(input: Comment): PublicComment {
    return { id: input.id, content: input.content }
  }

  includesMap = {
    author: async (input: Comment) => {
      const user = await this.cache.userProfile.call(input.userId)
      return { name: user.name, avatarUrl: user.picture }
    },
  }
}

Important Notes

  • Opt-in: Without configuration, behavior is identical to before (unlimited parallelism). Existing code is unaffected.
  • Instance-level: Limits are shared across all calls on the same transformer instance. In server environments where one instance handles multiple requests, concurrent requests share the same semaphore.
  • Batch methods only: transform() / _transform() are not affected by concurrency — only transformMany / _transformMany are throttled. Per-include limits still apply to both single and batch transforms.

API Reference

AbstractTransformer

| Member | Type | Description | |--------|------|-------------| | data(input, props) | protected abstract | Core transformation logic (must implement). Can return TOutput or Promise<TOutput>. | | includesMap | protected readonly | Map of include handlers. Each handler receives (input, props, forwardedIncludes). | | cache | public readonly | Record of Cache instances for data fetching. | | transformers | public | Register nested transformers for cache clearing propagation. | | transform({input, includes?, unsafeIncludes?, props?*}) | public | Transform a single object. | | transformMany({inputs, includes?, unsafeIncludes?, props?*}) | public | Transform an array of objects. | | includesConcurrency | protected readonly | Limits concurrent executions per include key. Applies to all transform methods. Define as class property. | | concurrency (constructor) | constructor param | Limits how many items transformMany processes in parallel. Pass via super({ concurrency: N }). | | clearCache() | public | Clear all caches (including nested transformers). |

*props is required when the transformer defines a Props type, optional otherwise.

Cache

| Method | Description | |--------|-------------| | new Cache(fn, options?) | Create a cache. fn must take 0 or 1 argument. options.on specifies which object properties to use as cache key. options.maxSize limits entries (oldest evicted). | | call(...args) | Call the cached function. Same-input calls return cached result. Concurrent calls share the same promise. | | clear() | Clear all cached results. |

Semaphore

| Method | Description | |--------|-------------| | new Semaphore(limit) | Create a semaphore with given concurrency limit. limit must be a positive integer. | | run(fn) | Execute fn (sync or async) when a slot is available. Returns Promise<T>. Queues if at capacity. |

Author

Created and maintained by Torben Köhler. Feel free to reach out via GitHub or LinkedIn.

License

MIT-NSR