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

@mnemonica/nestjs

v0.7.7

Published

NestJS adapter for mnemonica — data lineage, nominal typing, and hook-based observability

Readme

@mnemonica/nestjs

A NestJS adapter for the mnemonica stack — written by someone who got tired of losing the data when a request failed.

Here is the story this package tells. A request arrives as a plain JSON payload. You validate it, you construct your domain object from it, and somewhere six calls later something throws — and the error tells you what broke but not which data walked in the door and caused it. The payload is gone, the context is gone, and you are left archeologizing through logs.

The mnemonica stack exists to end that. Three layers, three packages:

  • mnemonica — instance inheritance: types defined via define(), constructed from parent instances, lineage carried in the prototype chain. Your objects remember where they came from.
  • @mnemonica/dive — execution-flow tracing: context pinned to instances, errors that carry their data, a bounded flight-recorder trace. Framework-agnostic.
  • this package — the wiring: hooks, pipe, interceptors, middleware, and DI tokens that connect both into the NestJS request lifecycle.

The payoff: when something fails in production, the error object itself carries the instance that failed and the flow that produced it. No correlation-id scavenger hunt. The data is the forensics.

Install

npm install @mnemonica/nestjs mnemonica @mnemonica/dive \
  class-validator class-transformer

Peer dependencies: @nestjs/common, @nestjs/core, reflect-metadata (and @opentelemetry/api only if you use the tracer options).

Dual build: ESM (import) and CommonJS (require) both work. The CJS flavor loads @mnemonica/dive through require(esm), so require() consumers need Node ≥ 20.19 / 22.

Step by step: a request becomes a traceable instance

Step 1 — define the type. This is plain mnemonica, no NestJS involved:

import { define } from 'mnemonica';
import { getPreRoot } from '@mnemonica/nestjs';

export const PaymentEntity = define(
  'PaymentEntity',
  function (this: PaymentEntityInstance, data: CreatePaymentDto) {
    // getPreRoot resolves THIS request's raw payloads (see step 2)
    const preRoot = getPreRoot(data);
    if (preRoot?.raw) {
      this.preRoot = preRoot.raw;
    }
    Object.assign(this, data);
  },
);

Step 2 — wire the module. thunderstruck: true attaches the dive lifecycle hooks to the collection and registers the pre-root interceptor globally:

import { Module } from '@nestjs/common';
import { MnemonicaModule } from '@mnemonica/nestjs';

@Module({
  imports: [
    MnemonicaModule.forRoot({
      autoExtract   : true,  // auto-extract returned instances
      thunderstruck : true,  // dive hooks + pre-root payloads
      traceLimit    : 1024,  // dive ring size = its memory bound
    }),
  ],
})
export class AppModule {}

Step 3 — validate the wire, then construct. The pipe validates a plain DTO with class-validator, then constructs the mnemonica instance:

import { Controller, Post, Body } from '@nestjs/common';
import { mvp } from '@mnemonica/nestjs';

@Controller('payments')
export class PaymentsController {
  @Post()
  create (
    @Body(mvp.forType(PaymentEntity, CreatePaymentDto))
    payment: InstanceType<typeof PaymentEntity>,
  ) {
    return payment; // a real mnemonica instance, lineage and all
  }
}

Why not @decorate() the DTO class itself? Because Nest / class-transformer instantiate DTOs with a zero-arg new and assign properties afterwards — the mnemonica instance would be created with empty __args__, and the constructor arguments are the data flow. The two-step pipe is not a workaround: the DTO stays the wire's validation schema, the mnemonica instance is where lineage begins.

Step 4 — return instances directly. With autoExtract, the global serializer interceptor calls .extract() on any mnemonica instance a controller returns, before JSON serialization. No manual mapping.

Step 5 — when it fails, the error carries the data. Dive pins the failed instance and its execution branch onto the error object itself, so an exception filter (or a process-level handler) can report which data caused the failure:

import { getErrorInstance, getFlow } from '@mnemonica/dive';

catch (err: unknown) {
  if (err instanceof Error) {
    const instance = getErrorInstance(err); // the data that failed
    const branch = getFlow(err);            // the flow that produced it
    // ... log / report / feed your APM
  }
}

MnemonicaModule.forRoot(options?)

Registers the global/default TypesCollection.

| Option | Type | Default | Description | |-----------------|-----------------------------------|----------------|-------------| | collection | TypesCollection | defaultTypes | Use a custom types collection | | autoExtract | boolean | false | Globally register MnemonicaSerializerInterceptor | | telemetry | boolean | false | Wire console telemetry hooks | | tracer | Tracer | undefined | OTel tracer — spans per construction, nested along the prototype chain | | traceDiveCalls | boolean | false | Requires tracer — span EVERY dive-wrapped call, parented on dive's trace; async spans close at settle | | traceLimit | number | unbounded | Dive ring-buffer size; applied only when provided, never overrides a direct setTraceLimit(). Unbounded since dive's 2026-09-02 flip (Number.MAX_SAFE_INTEGER); pass 1024 for the pre-flip bound | | thunderstruck | boolean \| ThunderstruckOptions | false | Dive hooks + global MnemonicaThunderstruckInterceptor; { storeRequest: true } also links the raw request into the record | | asyncFlow | boolean | false | ALS backbone (AsyncFlowProvider): unwrapped async hops (timers, promise continuations, async generators) attribute to the parental dive edge; context instances stay pinned for the request's lifetime so edge.instance never derefs to undefined mid-request. Root frame comes from mtm (HTTP) or runInScope(fn); read the active frame via currentFrame() — in an uncaughtException handler that IS the failing fiber's context |

The default trace limit is exported as DEFAULT_TRACE_LIMIT so the knob is discoverable where the module is configured:

import { DEFAULT_TRACE_LIMIT } from '@mnemonica/nestjs';

MnemonicaModule.forFeature(name, config?)

Creates an isolated TypesCollection per module. Pass mnemonica constructorOptions as the second argument:

@Module({
  imports: [
    MnemonicaModule.forFeature('payments', { strictChain: false }),
  ],
})
export class PaymentsModule {}

| Parameter | Type | Description | |-----------|----------------------|-------------| | name | string | Feature identifier for the injection token | | config | constructorOptions | Optional: strictChain, blockErrors, etc. |

Shortcuts: mti / mvp / mtm

The long names are for reading; the short aliases are for typing. They enable per-controller wiring instead of the global forRoot({ thunderstruck: true }):

import { mti, mvp } from '@mnemonica/nestjs';

@Controller('bundle')
@UseInterceptors(mti) // pre-root feed on THESE routes only
export class BundleController {
  @Post('invoice')
  createInvoice (
    @Body(mvp.forType(InvoiceEntity, CreateInvoiceDto))
    invoice: InstanceType<typeof InvoiceEntity>,
  ) {
    return invoice;
  }
}

| Alias | Full name | Kind | |-------|--------------------------------|------| | mti | MnemonicaThunderstruckInterceptor | pre-root payload feed, per-controller | | mvp | MnemonicaValidationPipe | validate DTO → construct instance | | mtm | MnemonicaTraceMiddleware | one OTel span per HTTP request, nested hooks |

mtm needs a Tracer and the MnemonicaOtelProvider in DI (it nests construction spans under the request span):

import type { NestModule, MiddlewareConsumer } from '@nestjs/common';

export class AppModule implements NestModule {
  configure (consumer: MiddlewareConsumer) {
    consumer.apply(mtm).forRoutes('*');
  }
}

Thunderstruck — pre-root request data

With thunderstruck: true (or @UseInterceptors(mti) per controller), the interceptor stashes { method, url, params, query, body, headers } before any construction happens — headers included on purpose: they carry the correlation ids (traceparent, x-request-id, …) that stitch the forensics to upstream traces. mvp then attaches the validated DTO alongside it.

Correlation is by object identity: the body / query / params / headers object the interceptor stamped is the same reference pipes and construct handlers receive — so a construct handler resolves its request's payloads with getPreRoot(data). No ALS, no cross-request races.

Lifetime is the request's own: payloads live in a WeakMap keyed on the request objects — no store to drain, no release step. When the request objects are garbage-collected, the payloads go with them. The record stays resolvable after construction for as long as the request object lives, so an exception filter can still report WHICH data caused a failure — that data is the forensics.

storeRequestforRoot({ thunderstruck: { storeRequest: true } }) — links the raw request object itself into the record (raw.request) and stamps it as a correlation key too, so code holding only the request — an exception filter with @Req(), say — resolves getPreRoot(req) directly. Retention is unchanged: the WeakMap's ephemeron semantics keep the record→request cycle collectable — the record still dies with the request. Standalone feeders get the same by passing request in the payload they hand to feedPreRoot.

Standalone usage (no NestJS)

Everything dive-related works without the framework. attachHooks wires any mnemonica collection to dive's lifecycle tracing; the pre-root store is plain functions you can feed from your own boundary:

import {
  attachHooks, feedPreRoot, getPreRoot,
} from '@mnemonica/nestjs';
import { defaultTypes } from 'mnemonica/module';

attachHooks(defaultTypes); // create edges + wrapped instance methods

// your own HTTP boundary (raw node, fastify, whatever):
function onRequest (method: string, url: string, body: unknown) {
  feedPreRoot({ method, url, body });
}

// inside any construct handler:
const preRoot = getPreRoot(data); // { raw, validated } of THIS request

Usage without mnemonica

If you don't use mnemonica, you don't need this adapter — use @mnemonica/dive directly. Its primitives work on plain objects and classes:

import { wrap, getErrorInstance, getFlow } from '@mnemonica/dive';

const processOrder = wrap(async (order) => {
  // ... your code
}, order); // context = the data itself

try {
  await processOrder(order);
} catch (err) {
  getErrorInstance(err); // → order
  getFlow(err);          // → the branch that produced the error
}

What you keep: wrap, current, getFlow, getErrorInstance, setTraceLimit — the whole engine. What you lose without mnemonica: the lifecycle wiring (create edges, automatic method wrapping via attachHooks), and everything this adapter adds on top (pipe, serializer, pre-root store, OTel spans).

Corners and caveats

  • DTOs stay DTOs. Nest/class-transformer instantiate DTOs with a zero-arg new and assign properties after — see step 3 for why the pipe is two-step by design.
  • file:-symlink development caveat. When the adapter is consumed through a file: link (monorepo local dev), each linked package carries its own class-transformer copy, and @Type(() => NestedDto) metadata is stored per-copy — nested DTO validation then silently degrades (nested objects stay plain and fail as unknownValue). Keep DTOs flat in that topology, or install with a single shared class-transformer. Real npm consumers are unaffected: semver dedupe leaves one copy. class-validator itself is immune — its metadata storage is globalThis-shared.
  • Primitive request bodies are not identity-correlatable. A string/number body can't key a WeakMap; it is still inside the raw pre-root payload, just not resolvable via getPreRoot.
  • Headers are fed, all of them. The record mirrors the request — including authorization if present — because headers carry the correlation ids (traceparent, x-request-id) that stitch forensics to upstream traces, and bodies carry credentials just as often. Detail reduction / redaction is a separate, planned task; until then treat pre-root payloads in reports the way you treat request logs.
  • Async constructors (mnemonica async types): consume them with const instance = await new MyAsyncType(data). Fire-and-forget construction makes failures surface as real process-level unhandledRejection — sometimes the demo you want, never the default.
  • traceLimit is dive's memory bound. The ring holds at most N edges, and edges strongly reference their instances until evicted. Keep raw third-party buffers OFF your instances (store an id/hash); shrink the limit under payload-heavy load.
  • Global vs per-controller thunderstruck. forRoot({ thunderstruck }) feeds every route; @UseInterceptors(mti) feeds only the routes you choose. Prefer per-controller when only some flows need forensics. The storeRequest flag is a forRoot option — per-controller wiring never registers the options token, so the interceptor keeps the default (no request link).

Other exports

MnemonicaBody — convenience decorator

Combines @Body() with the validation pipe:

@Post()
async create (
  @MnemonicaBody(PaymentEntity, { dtoClass: CreatePaymentDto })
  payment: InstanceType<typeof PaymentEntity>,
) {
  return payment;
}

isMnemonicaInstance(value)

Runtime type guard using getProps() — works across realms, no instanceof hacks:

import { isMnemonicaInstance } from '@mnemonica/nestjs';

if (isMnemonicaInstance(value)) {
  console.log(value.extract());
}

formatFlow(target?) / errorContext(error)

Read-side helpers over dive's trace — the two idioms error boundaries otherwise re-derive by hand:

import { formatFlow, errorContext } from '@mnemonica/nestjs';

// JSON-safe branch for reports / filters / logs (no live instance refs):
const flow = formatFlow(error);
// → [{ name, kind, status, duration }, …]  (duration: null while unsettled)

// "who failed": pinned instance first, ambient context as fallback:
const instance = errorContext(error);   // getErrorInstance(err) ?? current()

DiveOtelProvider — spans for every wrapped call

MnemonicaOtelProvider spans constructions; DiveOtelProvider subscribes to dive's edge lifecycle hooks and spans every dive-wrapped call (call / construct / method / recontext), named dive.<kind>:<name>. Parentage comes from dive's own trace (edge parentId), not ALS; at unwrapped boundaries the span nests under the active OTel span, so the HTTP request span adopts the whole branch. Async spans end at settle, not at the sync close.

// standalone:
import { DiveOtelProvider } from '@mnemonica/nestjs';
const diveOtel = new DiveOtelProvider(tracer);
diveOtel.attach();   // idempotent; diveOtel.detach() unsubscribes

// or via the module:
MnemonicaModule.forRoot({ tracer, traceDiveCalls: true });

Note: dive's clear() wipes hook subscribers — re-attach() after it.

InjectMnemonicaCollection(name?)

Inject a TypesCollection by feature name. Omit or use 'default' for the root collection:

@Injectable()
export class PaymentsService {
  constructor (
    @InjectMnemonicaCollection() private collection: TypesCollection,
  ) {}
}

Type safety with lookup()

Use lookup() from mnemonica for fully type-safe type retrieval when your TypeRegistry is augmented (e.g. via @mnemonica/tactica):

import { lookup } from 'mnemonica';

const PaymentEntity = lookup('PaymentEntity');
const payment = new PaymentEntity({ amount: 100 });

For AI agents

This package is deliberately agent-friendly. If you are an AI assistant working in a codebase that uses @mnemonica/nestjs, here is the map:

  • The mental model is small. Requests become instances at the validation pipe (mvp); instances remember their request via the pre-root store (getPreRoot); errors carry their instance via dive (getErrorInstance). Everything else is wiring.
  • Do not flatten the two-step pipe. If you see @Body(mvp.forType(Type, Dto)), the DTO validates the wire and the mnemonica constructor receives the validated data. Collapsing these into a decorated DTO class breaks construction — see step 3.
  • getPreRoot(data) only resolves by object identity. It works inside construct handlers that receive the exact request payload object. It will not resolve a re-parsed copy, a primitive body, or data from a different request.
  • Prefer per-controller wiring (@UseInterceptors(mti)) over the global thunderstruck: true when only some routes need forensics.
  • The full contributor contract — architecture, invariants, and the testing gate — lives in AGENTS.md.

License

MIT