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

@nage-api/core

v1.0.0-beta.4

Published

Cross-cutting primitives for @nage-api applications — context, response envelope, error catalog, bootstrap

Readme

@nage-api/core

The small mandatory layer every @nage-api application depends on (PLAN.md §7.2).

// apps/api/src/main.ts — the whole file
import 'reflect-metadata';

import { bootstrap } from '@nage-api/core';

import { AppModule } from './app.module.js';
import { EnvSchema } from './config/env.schema.js';
import { buildConfig } from './config/nage.config.js';

const env = EnvSchema.parse(process.env);

void bootstrap({ module: AppModule, config: buildConfig(env) });

A generated main.ts calls @nage-api/config's loadEnvOrExit(EnvSchema) in place of parse: same validation, but it reports every invalid variable at once and exits non-zero instead of throwing on the first one.

// apps/api/src/app.module.ts
import { Module } from '@nestjs/common';
import { NageCoreModule } from '@nage-api/core';

import { EnvSchema } from './config/env.schema.js';
import { buildConfig } from './config/nage.config.js';
import { WidgetController } from './widget/widget.controller.js';

const config = buildConfig(EnvSchema.parse(process.env));

@Module({ imports: [NageCoreModule.forRoot(config)], controllers: [WidgetController] })
export class AppModule {}

forRoot is what registers the interceptor, the filter, the guards and the validation pipe. Skip it and the application still starts — with none of them, so the envelope, the error catalog and the request context are simply absent.

What it provides

| Area | Export | Notes | | ----------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | Request context | RequestContextService, getActiveContext() | AsyncLocalStorage-backed; carries requestId, user, tenantId (§18) | | Response envelope | ResponseInterceptor, successEnvelope | Controllers return data; no @Res() (§16.1) | | Errors | AllExceptionsFilter, NageError + catalog | Stable codes, safe messages, detail logged (§17) | | Decorators | @Public, @Owner, @OptionalOwner, @NoEnvelope | @Owner() returns a typed AuthUser (§13) | | Versioning | @ForVersion, @FromVersion, @TillVersion, @BetweenVersions, ApiVersionGuard | Header x-application-version, retained from the legacy framework (§16.2) | | Jobs | createJob, paginate, emptyPage | Typed Job<TEntity> for the Phase 5 data layer (§14.1) | | Logging | JsonLogger, NestLoggerAdapter | NDJSON with redaction, behind LoggerPort (§18) | | Security baseline | RateLimitGuard, @RateLimit, randomToken, redact, auditSecurity | CSPRNG, redaction, TLS policy, global throttle, boot-time audit (§12) | | DI tokens | NAGE_CONFIG, NAGE_LOGGER, repositoryToken(), … | How feature packages collaborate without importing each other (§7.3) | | Bootstrap | bootstrap(), createApplication() | helmet, CORS allow-list, compression, versioning (§21) | | Lifecycle | installShutdown, installProcessGuards, LifecycleState, RequestDrain | Ordered drain, readiness phase, fatal-error policy (§21) | | Request timeout | RequestTimeoutInterceptor, @RequestTimeout, @SkipRequestTimeout | 30 s by default; a handler that overruns is answered 504 (§21) |

Behaviour worth knowing

The envelope is a guarantee. Every HTTP response is { success, data | error, meta }. A repository page is lifted automatically: records becomes data, pagination goes to meta.pagination. The single opt-out is @NoEnvelope(), for downloads and webhook callbacks that need an exact body.

Errors never leak. A thrown NageError returns its code and safeMessage; its meta is logged, never serialised. Anything unrecognised becomes INTERNAL_ERROR / "Internal server error" with the real message, stack and cause in the log line — keyed by the same requestId the caller received. ValidationPipe failures become VALIDATION_FAILED (422) with field details.

Correlation ids are settled once. A caller-supplied X-Request-Id is validated against REQUEST_ID_PATTERN and replaced if it does not match — it is echoed into responses and log lines, so it is untrusted input. The id is in the response header, in meta.requestId, and on every log line, for successes and failures alike.

Defaults are the safe ones. CORS is off unless an allow-list is configured, because app.enableCors() with no arguments allowed every origin by omission; a wildcard must be spelled out and is refused in production. Helmet is on, HSTS in production. forbidNonWhitelisted makes an unexpected body property an error rather than a silent drop. Throttling is global and opt-out: the legacy framework imported a throttler and never registered a guard, so exactly one endpoint was limited. A critical security finding at boot exits non-zero instead of serving traffic insecurely.

Shutdown runs in an order, and the order is the point. bootstrap claims SIGTERM and SIGINT itself rather than calling Nest's app.enableShutdownHooks(), which destroys modules before it closes the HTTP server: measured against a 600 ms handler signalled at 200 ms, the handler's query returned pool is closed and the caller got that as a 200. installShutdown fails readiness first (/health/ready → 503, /health/live still 200), stops accepting connections, waits out the requests it already has under drainTimeoutMs, and only then calls app.close() so modules can close their pools. forceExitAfterMs bounds the whole thing and exits non-zero on expiry — before that, one handler that never returned kept a signalled process alive indefinitely.

Handlers have a deadline. http.requestTimeoutMs defaults to 30 seconds and answers 504 in the error envelope with the code REQUEST_TIMEOUT. It bounds the caller's wait, not the process's work: Node cannot abort a running promise, so put a deadline on the dependency as well. @RequestTimeout(ms) raises it for one route, @SkipRequestTimeout() removes it for a stream.

An uncaught exception or unhandled rejection is fatal. That is Node's verdict and it is kept; what is added is one structured fatal line before the process goes, and one bounded attempt at the graceful shutdown above, so the reason lands in the same log stream as everything else and the pools still close.

Redaction fails closed. redact masks configured field names at any depth, including inside arrays. Where it cannot see — past its depth bound, or into a cycle — it substitutes a marker rather than passing the value through, and the human-readable log format is scrubbed exactly like the JSON one. A log call never throws: an unreadable field is reported as unreadable, and the line still carries its level and message.

Dependency rule

@nage-api/core depends on @nage-api/contracts and nothing else — never a feature package, never a project entity. Enforced by @nage-api/eslint-config and pnpm check:boundaries.

Not yet implemented

  • A shared rate-limit store. The counters are per process, so a limit of 100 across four pods is 400. RateLimitStore is the seam, but the token forRoot binds it to is not exported, so today there is no way to supply another one. See docs/packages/core.md.
  • http.bodyLimit. Declared in the config type and read by nothing; body size is whatever the platform adapter defaults to.
  • URI versioning, Idempotency-Key, Deprecation/Sunset headers — §16.2 asks for all three; only header versioning is built.
  • pino. §18 names it as the production logger; JsonLogger covers structured output today and LoggerPort is the seam it would arrive behind.

Health and readiness endpoints, metrics and tracing live in @nage-api/observability; config loading and env validation in @nage-api/config.

The deeper guide is docs/packages/core.md.