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

@camcima/nestjs-deprecation

v1.1.0

Published

NestJS library for RFC 9745 (Deprecation) and RFC 8594 (Sunset) HTTP response headers, with Swagger and OpenTelemetry integration

Readme

CI CodeQL codecov npm version License: MIT TypeScript Node.js

NestJS library for RFC 9745 (Deprecation) and RFC 8594 (Sunset) HTTP response headers — decorator-driven API deprecation with Swagger and OpenTelemetry integration.

TL;DR

Announce that an API endpoint is deprecated — and when it will be switched off — in a standard, machine-readable way, with a single decorator. Consumers learn about it from the response itself instead of a changelog or an email. You also get opt-in Swagger docs and an optional OpenTelemetry metric that answers "who is still calling this?" — two extra lines of setup each, see below.

npm install @camcima/nestjs-deprecation
import { Module, Controller, Get } from '@nestjs/common';
import { DeprecationModule, Deprecated } from '@camcima/nestjs-deprecation';

// 1. Register once in your root module.
@Module({ imports: [DeprecationModule.forRoot()] })
export class AppModule {}

// 2. Decorate the endpoint (or the whole controller).
@Controller('orders')
export class OrdersController {
  @Deprecated({ deprecatedAt: '2026-07-01', sunsetAt: '2027-01-01', successor: '/v2/orders' })
  @Get()
  list() {
    /* ... */
  }
}

Every response from the decorated endpoint now carries:

Deprecation: @1782864000
Sunset: Fri, 01 Jan 2027 00:00:00 GMT
Link: </v2/orders>; rel="successor-version"

The library is strictly informational — it only adds headers (and optionally fires a telemetry hook); it never changes or blocks endpoint behavior. See Quick Start for the full walkthrough.

Table of Contents

What are RFC 9745 / RFC 8594?

RFC 9745 (published 2025) defines the Deprecation HTTP response header: a machine-readable signal that a resource has been, or will be, deprecated as of a given date. RFC 8594 defines the companion Sunset header: the date after which the resource is expected to stop responding. Together with an RFC 8288 Link header pointing clients at deprecation docs and/or a successor endpoint, they give API consumers a standard way to detect and react to deprecations without out-of-band communication (changelogs, emails, Slack messages).

To our knowledge, @camcima/nestjs-deprecation is the first NestJS implementation of RFC 9745.

A deprecated endpoint's response looks like this:

HTTP/1.1 200 OK
Deprecation: @1782864000
Sunset: Fri, 01 Jan 2027 00:00:00 GMT
Link: <https://docs.example.com/deprecations/orders-v1>; rel="deprecation", </v2/orders>; rel="successor-version"
  • Deprecation carries an RFC 9651 structured-field date: @ followed by a Unix timestamp in seconds.
  • Sunset carries an HTTP-date (IMF-fixdate) and is only sent when a sunset date is configured.
  • Link carries rel="deprecation" (RFC 9745, pointing at documentation) and rel="successor-version" (RFC 5829, pointing at the replacement), plus any other relations you add.

Installation

npm install @camcima/nestjs-deprecation
pnpm add @camcima/nestjs-deprecation

Peer dependencies

| Package | Version | Required | | -------------------- | --------------------------------- | ------------------------------------------ | | @nestjs/common | ^10.0.0 \|\| ^11.0.0 | Yes | | @nestjs/core | ^10.0.0 \|\| ^11.0.0 | Yes | | reflect-metadata | ^0.1.13 \|\| ^0.2.0 | Yes | | @nestjs/swagger | ^7.0.0 \|\| ^8.0.0 \|\| ^11.0.0 | No (optional, for the ./swagger subpath) | | @opentelemetry/api | >=1.8.0 | No (optional, for the ./otel subpath) |

Quick Start

Register DeprecationModule.forRoot() once in your root AppModule. It registers a global APP_INTERCEPTOR that writes deprecation headers on any handler or controller decorated with @Deprecated(). Accidentally importing it twice is tolerated — the first interceptor instance to see a request wins and later ones skip it, so headers and telemetry are never duplicated — but register it once, in the root module: which registration runs first follows module resolution order, so with two registrations it is the winner's options (and its onDeprecatedCall) that take effect.

// app.module.ts
import { Module } from '@nestjs/common';
import { DeprecationModule } from '@camcima/nestjs-deprecation';

@Module({
  imports: [DeprecationModule.forRoot()],
})
export class AppModule {}

Decorate a handler (or an entire controller class) with @Deprecated():

import { Controller, Get } from '@nestjs/common';
import { Deprecated } from '@camcima/nestjs-deprecation';

@Controller('orders')
export class OrdersController {
  @Deprecated({
    deprecatedAt: '2026-07-01T00:00:00Z',
    sunsetAt: '2027-01-01T00:00:00Z',
    link: 'https://docs.example.com/deprecations/orders-v1',
    successor: '/v2/orders',
    links: [{ rel: 'latest-version', href: '/v3/orders', type: 'application/json' }],
    note: 'Use POST /v2/orders',
  })
  @Get()
  list() {
    /* ... */
  }
}

That produces exactly the response headers shown above:

Deprecation: @1782864000
Sunset: Fri, 01 Jan 2027 00:00:00 GMT
Link: <https://docs.example.com/deprecations/orders-v1>; rel="deprecation", </v2/orders>; rel="successor-version"

(The links escape hatch above adds a third, custom rel="latest-version" link into the same header; it is omitted from the example response for brevity. note is never sent on the wire — it only surfaces in Swagger docs and in the telemetry event.)

@Deprecated() options

| Option | Type | Required | Description | | -------------- | ---------------- | -------- | ---------------------------------------------------------------------------------------- | | deprecatedAt | Date \| string | Yes | When the endpoint is (or will be) deprecated. May be in the future. | | sunsetAt | Date \| string | No | When the endpoint stops working. Must not be earlier than deprecatedAt. | | link | string | No | Deprecation documentation URL — emitted as Link; rel="deprecation" (RFC 9745). | | successor | string | No | Replacement endpoint — emitted as Link; rel="successor-version" (RFC 5829). | | links | LinkRelation[] | No | Escape hatch for arbitrary RFC 8288 relations, appended after link/successor. | | note | string | No | Human note; never sent on the wire. Surfaces in Swagger docs and in the telemetry event. |

Both dates accept a Date or an ISO 8601 string. A string carrying a time must include a timezone designator (2026-07-01T00:00:00Z or ...+02:00); without one it would be read in the server's local timezone, making the emitted header depend on where the app runs. Date-only strings (2026-07-01) are unambiguous UTC and always fine.

Invalid options — unparseable or untyped dates (including a unix timestamp passed as a number), a sunsetAt before deprecatedAt, a malformed URL/path, a links entry repeating the link/successor relation — throw at decoration time, i.e. when your application boots, rather than on the first matching request, so misconfiguration fails loudly and early. @Deprecated() can decorate a single handler method or an entire controller class; a method-level decorator overrides a class-level one on that method. Applying it to anything else (a getter, a property, a static method) throws too, rather than silently doing nothing.

Async configuration

DeprecationModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    enabled: config.get<boolean>('DEPRECATION_HEADERS_ENABLED', true),
  }),
});

forRootAsync also accepts useClass or useExisting, pointing at a class that implements DeprecationOptionsFactory:

import { DeprecationModuleOptions, DeprecationOptionsFactory } from '@camcima/nestjs-deprecation';

@Injectable()
export class DeprecationConfig implements DeprecationOptionsFactory {
  constructor(private readonly config: ConfigService) {}

  createDeprecationOptions(): DeprecationModuleOptions {
    return { enabled: this.config.get<boolean>('DEPRECATION_HEADERS_ENABLED', true) };
  }
}

DeprecationModule.forRootAsync({ imports: [ConfigModule], useClass: DeprecationConfig });

DeprecationModuleOptions accepts:

| Option | Type | Default | Description | | ------------------ | ---------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | enabled | boolean | true | Kill switch. forRoot({ enabled: false }) does not register the interceptor at all, so it costs nothing per request; forRootAsync resolves the flag at runtime and short-circuits instead. | | onDeprecatedCall | (event) => void \| Promise<void> | — | Invoked on every request to a deprecated endpoint, inline before the handler runs. See Telemetry. Errors — thrown synchronously or via a rejected promise — are caught and logged; they never affect the response. Defer slow work off the request path. |

Swagger integration

The ./swagger subpath is optional and requires @nestjs/swagger (already a common dependency in NestJS apps). It discovers every @Deprecated()-decorated controller/handler via DiscoveryService and:

  1. Sets deprecated: true on the OpenAPI operation.
  2. Appends a generated Markdown block to the operation description (deprecation date, sunset date, note, links) — merged with, not clobbering, any @ApiOperation() you already applied.
  3. Documents the Deprecation / Sunset / Link response headers with example values on every response of the operation.
  4. Stamps the x-sunset extension on the operation when sunsetAt is set, so diff tools can enforce the deprecation window (see Breaking-change detection).

Add DiscoveryModule (from @nestjs/core) to your application module, create the OpenAPI document, and pass it through applyDeprecationDocs(document, app). The transform mutates and returns the given document instance only — it never touches decorator metadata, so you can build multiple differently-filtered documents in any order and each one is independent.

// app.module.ts
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { DeprecationModule } from '@camcima/nestjs-deprecation';

@Module({
  imports: [DiscoveryModule, DeprecationModule.forRoot()],
})
export class AppModule {}
// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { applyDeprecationDocs } from '@camcima/nestjs-deprecation/swagger';

const config = new DocumentBuilder().setTitle('My API').build();
const document = SwaggerModule.createDocument(app, config);

SwaggerModule.setup('/api', app, applyDeprecationDocs(document, app));

applyDeprecationDocs(document, app, options?) accepts an optional filter callback to skip specific controllers per document (and xSunset, below):

const publicDocument = applyDeprecationDocs(SwaggerModule.createDocument(app, config), app, {
  filter: (controller) => controller.name !== 'InternalController',
});

If DiscoveryModule is not imported, applyDeprecationDocs throws a clear setup error naming the fix, rather than failing silently.

Routes are matched by recomputing each handler's route path. An application-wide setGlobalPrefix() is read from the application and applied exactly, and handlers inherited from a base controller class are documented like any other. Paths using route parameters, named wildcards (*splat), or optional-parameter groups ({/:id}) are translated to their OpenAPI form, and request methods beyond the OpenAPI eight (such as SEARCH) are covered. Handlers whose document path still cannot be resolved (e.g. custom URI versioning, per-route prefixes) fall back to an unambiguous suffix match, and are skipped with a logged warning rather than mis-annotated. One caveat: for documents built with the include option, a deprecated route excluded from the document can suffix-match a similarly named route from another module — prefer filtering with the filter callback (which skips the controller entirely) over relying on include alone.

Breaking-change detection (x-sunset)

Deprecating an operation is not a breaking change; removing it is. oasdiff — and other tools that diff two OpenAPI documents in CI — resolve that by reading an x-sunset extension alongside deprecated: true, and treating removal before that date as breaking:

/orders:
  get:
    deprecated: true
    x-sunset: '2027-01-01'

applyDeprecationDocs emits it for you from the sunsetAt you already passed to @Deprecated() — there is nothing extra to declare, and the extension cannot drift from the Sunset header or the description block, because all three derive from the same frozen value. The value is an RFC 3339 full-date (YYYY-MM-DD), the form oasdiff documents.

It is on by default: x-sunset is an inert OpenAPI extension, ignored by tooling that does not recognize it, and nothing about it reaches the wire. Opt out with xSunset: false, and note that an x-sunset you authored yourself (e.g. via @ApiExtension()) is left untouched:

applyDeprecationDocs(document, app, { xSunset: false });

Two caveats. Operations deprecated without a sunsetAt get no x-sunset — which is correct, but under oasdiff's --deprecation-days-stable / --deprecation-days-beta grace-period enforcement the extension is mandatory, so those endpoints will fail that check until you give them a sunset date. And oasdiff currently reads x-sunset only at the operation level; a class-level @Deprecated() is fine, since it is stamped onto each of that controller's operations.

applyDeprecationDocs is independent of the enabled kill switch: it decorates the OpenAPI document at build time regardless of the runtime enabled setting, so if you disable the interceptor at runtime, stop calling applyDeprecationDocs too, to keep docs and runtime behavior in sync.

Telemetry

DeprecationModuleOptions.onDeprecatedCall fires once per request to a deprecated endpoint, after the response headers are written:

interface DeprecatedCallEvent {
  method: string;
  route: string; // route PATTERN, e.g. "/orders/:id" — not the concrete URL, to keep metric cardinality low
  controllerName: string;
  handlerName: string;
  metadata: DeprecationMetadata; // the frozen, precomputed decorator options
  isPastSunset: boolean;
}

Use it for logging, custom metrics, or any sink you like:

DeprecationModule.forRoot({
  onDeprecatedCall: (event) => {
    myMetrics.increment('deprecated_requests', { route: event.route });
  },
});

./otel — ready-made OpenTelemetry listener

The ./otel subpath is optional and requires @opentelemetry/api. It calls only the OTel API (never creates or configures a MeterProvider) and defaults to the global meter registry:

import { DeprecationModule } from '@camcima/nestjs-deprecation';
import { createOtelDeprecationListener } from '@camcima/nestjs-deprecation/otel';

DeprecationModule.forRoot({
  onDeprecatedCall: createOtelDeprecationListener(),
});

Pass an explicit meterProvider if you don't want the global one:

createOtelDeprecationListener({ meterProvider });

This registers a counter named http.server.deprecated_requests with attributes:

| Attribute | Type | Notes | | ------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------- | | http.request.method | string | e.g. "GET" | | http.route | string | route pattern, e.g. "/orders/:id" | | deprecation.past_sunset | boolean | whether the current time is past the configured sunset date | | deprecation.sunset_date | string | ISO 8601 sunset date; omitted when no sunsetAt is set (keeps cardinality low — one value per route, not per request) |

Strictly informational

@camcima/nestjs-deprecation never changes endpoint behavior — not before the deprecation date, not after the sunset date. This is the purest reading of RFC 9745: "the act of deprecation does not change any behavior of the resource." There is no 410-enforcement mode, no brownout/probabilistic-failure mode, and no request blocking of any kind; the library only ever adds response headers and (optionally) fires a telemetry callback.

Enforcement behaviors like returning 410 Gone past sunset, or scheduled brownouts, were deliberately considered and left out of scope. If you need enforcement, build it on top of the onDeprecatedCall/isPastSunset hook rather than expecting this library to do it for you.

Known limitations

  • Guards run before interceptors. A request rejected by a guard (401/403 from auth, 429 from throttling) never reaches the interceptor, so it carries no Deprecation/Sunset/Link headers and fires no onDeprecatedCall event. Clients whose credentials have expired — often the stalest integrations, and the ones most likely to be on a deprecated endpoint — will not see the signal, and migration dashboards undercount deprecated traffic by the guard-rejected share.
  • Decorator composition. The metadata is stored on the handler function itself. A third-party decorator that replaces descriptor.value with a wrapper (rather than mutating it in place, as Nest's own decorators do) and is applied after @Deprecated() will drop the metadata, silently un-deprecating the endpoint. If you compose with wrapping decorators, keep @Deprecated() above them.
  • Header write ordering. The interceptor writes the Deprecation/Sunset/Link headers before calling the route handler (next.handle()), so that they still land on thrown exceptions and streaming responses. A consequence: anything that sets a Link header after that point — e.g. inside the handler body itself, or in an interceptor registered to run closer to the handler — will overwrite rather than merge with the deprecation Link value. Middleware or an interceptor registered before DeprecationModule's (so it runs first) is appended to correctly instead of overwritten.

API Reference

| Export | Kind | Description | | ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------- | | Deprecated | Decorator | Method or class decorator; emits DeprecatedOptions as metadata | | DeprecationModule | Class | Dynamic module. Use forRoot(options?) or forRootAsync(options) | | DeprecationInterceptor | Injectable class | Global interceptor registered by DeprecationModule; writes headers and fires onDeprecatedCall | | DeprecatedOptions | Interface | Options accepted by @Deprecated() | | DeprecationMetadata | Interface | Precomputed, frozen wire values stored as Reflect metadata | | DeprecatedCallEvent | Interface | Shape of the event passed to onDeprecatedCall | | DeprecatedCallListener | Type | `(event: DeprecatedCallEvent) => void \| Promise<void>` | | DeprecationModuleOptions | Interface | Options accepted by forRoot() | | DeprecationModuleAsyncOptions | Interface | Options accepted by forRootAsync() (useFactory, useClass, or useExisting) | | DeprecationOptionsFactory | Interface | Implemented by the class given to forRootAsync({ useClass }) / ({ useExisting }) | | LinkRelation | Interface | { rel: string; href: string; type?: string } — one entry in links | | DEPRECATION_METADATA_KEY | Constant | Reflect metadata key under which @Deprecated() stores DeprecationMetadata | | DEPRECATION_MODULE_OPTIONS | Symbol | DI token for the module options |

Swagger subpath (@camcima/nestjs-deprecation/swagger):

| Export | Kind | Description | | ----------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | applyDeprecationDocs | Function | Transforms the given OpenAPI document: marks @Deprecated() operations deprecated and documents their response headers. Signature: applyDeprecationDocs(document, app, options?) → returns the document. | | DeprecationDocumentLike | Interface | Minimal structural view of the OpenAPI document accepted by applyDeprecationDocs | | ApplyDeprecationDocsOptions | Interface | Options for applyDeprecationDocs (filter) | | DiscoveredController | Interface | Structural controller view passed to the filter option |

OTel subpath (@camcima/nestjs-deprecation/otel):

| Export | Kind | Description | | -------------------------------- | --------- | ---------------------------------------------------------------------- | | createOtelDeprecationListener | Function | Returns an onDeprecatedCall listener that increments an OTel counter | | OtelDeprecationListenerOptions | Interface | Options for createOtelDeprecationListener (meterProvider) | | DEPRECATED_REQUESTS_METRIC | Constant | 'http.server.deprecated_requests' |

License

MIT