Skip to content

Repository files navigation

nestjs-sunset

RFC-compliant API deprecation lifecycle for NestJS. One decorator. Three standards. Zero configuration.

npm version License: MIT CI Coverage


The problem

Deprecating a NestJS endpoint today means three disconnected annotations, a Unix timestamp you compute by hand, and zero visibility into who is still calling the old route.

// Before — fragile, incomplete, non-standard
@Get('v1/users')
@ApiOperation({ deprecated: true })       // ← Swagger only, runtime gets nothing
@Header('Deprecation', '@1688169599')     // ← timestamp you computed manually
async listUsersLegacy() { ... }
//                                        ← No Sunset header
//                                        ← No Link to migration docs
//                                        ← No pre-sunset alert at startup
//                                        ← No call counter to know who migrated
// After — one decorator, three RFC headers, automated everything
@Deprecated({
  deprecatedAt: '2025-06-01',
  sunset:       '2026-01-01',
  link:         '/docs/migration/v2-users',
})
@Get('v1/users')
async listUsersLegacy() { ... }

Every response from /v1/users now carries:

HTTP/1.1 200 OK
Deprecation: @1748736000
Sunset: Wed, 01 Jan 2026 00:00:00 UTC
Link: </docs/migration/v2-users>; rel="deprecation"; type="text/html"

Quick Start

1. Install

npm install nestjs-sunset

2. Import the module

// app.module.ts
import { Module } from '@nestjs/common';
import { SunsetModule } from 'nestjs-sunset';

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

3. Decorate your endpoint

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

@Controller('v1')
export class UsersController {
  @Deprecated({
    deprecatedAt: '2025-01-01',
    sunset: '2026-01-01',
    link: '/docs/migration/v2-users',
    message: 'Migrate to GET /v2/users — supports cursor-based pagination.',
  })
  @Get('users')
  listUsers() {
    return [];
  }
}

That's it. Deprecation, Sunset, and Link headers are injected into every response automatically. No middleware, no filters, no further configuration.

Zero-arg usage@Deprecated() works with no options. The Deprecation header will reflect the current deployment time. Add sunset and link when you know them.


Standards

nestjs-sunset implements three IETF standards and emits no custom or proprietary headers.

Standard Header emitted Format Purpose
RFC 9745 Deprecation @<unix_seconds> Signals when the resource was deprecated
RFC 8594 Sunset IMF-fixdate Signals when the resource will stop responding
RFC 8288 Link <url>; rel="deprecation" Links to the migration documentation

The Deprecation and Sunset headers use intentionally different date formats — an asymmetry documented in RFC 9745 §3 ("for historical reasons"). nestjs-sunset handles both formats transparently; you supply a Date, a string, or a number and the correct format is computed automatically.


Options

@Deprecated(options?)

All options are optional. Calling @Deprecated() with no arguments is valid.

Option Type Default Emits
deprecatedAt Date | string | number Date.now() Deprecation: @<unix_seconds> (RFC 9745)
sunset Date | string | number Sunset: <IMF-fixdate> (RFC 8594)
link string Link: <url>; rel="deprecation"; type="text/html" (RFC 8288)
message string Structured log entry only — never emitted as an HTTP header

Input flexibilitydeprecatedAt and sunset accept all three forms:

@Deprecated({ deprecatedAt: new Date('2025-01-01') })          // Date object
@Deprecated({ deprecatedAt: '2025-01-01' })                    // ISO 8601 string
@Deprecated({ deprecatedAt: 1735689600000 })                   // Unix ms number

Validation at startup — If sunset is not strictly after deprecatedAt, the application refuses to start with an actionable error message that cites RFC 9745 §4 and names the endpoint.


Module configuration

SunsetModule.forRoot(options?)

SunsetModule.forRoot({
  isGlobal: true, // register as NestJS global module (recommended)
  logLevel: 'warn', // log level for deprecated-endpoint calls
  sunsetWarningThresholdDays: 60, // warn at startup if sunset is within N days
  onDeprecatedEndpointCalled: (event) => {
    metrics.increment('api.deprecated', { endpoint: event.endpoint });
  },
});
Option Type Default Description
isGlobal boolean false Register as a global NestJS module — DeprecationRegistry becomes injectable everywhere without explicit imports.
logLevel LogLevel 'warn' NestJS log level for structured log entries on each deprecated request.
sunsetWarningThresholdDays number 30 Emit a startup warning when an endpoint's sunset date is within this many days.
onDeprecatedEndpointCalled (event: DeprecationEvent) => void Hook called on every request to a deprecated endpoint. Useful for Datadog, Prometheus, or Slack alerts.

SunsetModule.forRootAsync(options)

Use this when module options come from a ConfigService or another async provider:

SunsetModule.forRootAsync({
  isGlobal: true,
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    sunsetWarningThresholdDays: config.get<number>('SUNSET_WARNING_DAYS', 30),
    onDeprecatedEndpointCalled: (event) => {
      // event.endpoint        → "GET /v1/users"
      // event.daysUntilSunset → 42  (null if no sunset configured)
      // event.timestamp       → Date of the request
      // event.options         → the full @Deprecated() options
    },
  }),
});

Introspection

DeprecationRegistry tracks every deprecated endpoint registered at startup and counts calls in memory since the last restart. Inject it anywhere in your application:

import { Injectable } from '@nestjs/common';
import { DeprecationRegistry } from 'nestjs-sunset';

@Injectable()
export class MonitoringService {
  constructor(private readonly registry: DeprecationRegistry) {}

  getSummary() {
    return this.registry.getAll();
    // ReadonlyArray<{
    //   routeDescription: string    // "GET /v1/users"
    //   options:          DeprecatedOptions
    //   callCount:        number    // requests since last restart
    //   lastCalledAt:     Date | null
    // }>
  }
}

DeprecationRegistry is available without explicit import when isGlobal: true is set on SunsetModule.forRoot().


Compatibility

NestJS 10 NestJS 11
Express
Fastify

Node.js — Requires Node.js ≥ 20.0.0.

@nestjs/swagger — Optional. When @nestjs/swagger is present in your project, @Deprecated() automatically sets deprecated: true on the corresponding OpenAPI operation. No configuration needed — the detection is automatic via a silent dynamic import.

Peer dependencies@nestjs/common, @nestjs/core, reflect-metadata, rxjs. No additional runtime dependencies.


License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages